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.
@@ -1,9 +1,11 @@
1
1
  // piggy/register/index.ts
2
2
  import { PiggyClient } from "../client";
3
3
  import logger from "../logger";
4
- import { routeRegistry, keepAliveSites, type RouteHandler, type BeforeMiddleware } from "../server";
4
+ import { routeRegistry, keepAliveSites, type RouteHandler, type BeforeMiddleware, type RouteDetail } from "../server";
5
5
  import { randomDelay, humanTypeSequence } from "../human";
6
6
  import { buildRespondScript, buildModifyResponseScript } from "../intercept/scripts";
7
+ import { storeRecord } from "../store";
8
+ import { TabPool } from "../pool";
7
9
 
8
10
  let globalClient: PiggyClient | null = null;
9
11
  export let humanMode = false;
@@ -25,13 +27,24 @@ async function retry<T>(label: string, fn: () => Promise<T>, retries = 2, backof
25
27
  throw last;
26
28
  }
27
29
 
28
- export function createSiteObject(name: string, registeredUrl: string, client: PiggyClient, tabId: string) {
30
+ export function createSiteObject(
31
+ name: string,
32
+ registeredUrl: string,
33
+ client: PiggyClient,
34
+ tabId: string,
35
+ pool?: TabPool
36
+ ) {
29
37
  let _currentUrl: string = registeredUrl;
38
+ let _modifyRuleCounter = 0;
39
+
40
+ // ── helpers ────────────────────────────────────────────────────────────────
41
+ // If pool exists, run fn with a pool tab. Otherwise use the fixed tabId.
42
+ function withTab<T>(fn: (t: string) => Promise<T>): Promise<T> {
43
+ return pool ? pool.withTab(fn) : fn(tabId);
44
+ }
30
45
 
31
- // ── Event listeners store ──────────────────────────────────────────────────
32
46
  const _eventListeners = new Map<string, Set<(data: any) => void>>();
33
47
 
34
- // Wire the client-level navigate event into site-level listeners
35
48
  const _unsubNavigate = client.onEvent("navigate", tabId, (url: string) => {
36
49
  _currentUrl = url;
37
50
  const handlers = _eventListeners.get("navigate");
@@ -51,59 +64,66 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
51
64
  }
52
65
  };
53
66
 
54
- // ── Intercept helper: unique fn name per pattern ───────────────────────────
55
- let _modifyRuleCounter = 0;
56
-
57
67
  const site: any = {
58
68
  _name: name,
59
69
  _tabId: tabId,
70
+ _pool: pool ?? null,
71
+
72
+ // ── Pool stats ────────────────────────────────────────────────────────────
73
+ poolStats: () => pool?.stats ?? null,
60
74
 
61
- // ── Navigation ─────────────────────────────────────────────────────────────
75
+ // ── Navigation ────────────────────────────────────────────────────────────
62
76
  navigate: (url?: string, opts?: { retries?: number }) => {
63
77
  const target = url ?? registeredUrl;
64
- return retry(name, async () => {
65
- logger.network(`[${name}] navigating ${target}`);
66
- await client.navigate(target, tabId);
67
- _currentUrl = target;
68
- }, opts?.retries ?? 2);
78
+ return withTab(t =>
79
+ retry(name, async () => {
80
+ logger.network(`[${name}] navigating → ${target}`);
81
+ await client.navigate(target, t);
82
+ _currentUrl = target;
83
+ }, opts?.retries ?? 2)
84
+ );
69
85
  },
70
86
 
71
- reload: () => client.reload(tabId),
72
- goBack: () => client.goBack(tabId),
73
- goForward: () => client.goForward(tabId),
74
- waitForNavigation: () => client.waitForNavigation(tabId),
87
+ reload: () => withTab(t => client.reload(t)),
88
+ goBack: () => withTab(t => client.goBack(t)),
89
+ goForward: () => withTab(t => client.goForward(t)),
90
+ waitForNavigation: () => withTab(t => client.waitForNavigation(t)),
91
+
92
+ title: () => withTab(async t => {
93
+ const title = await client.getTitle(t);
94
+ logger.info(`[${name}] title: ${title}`);
95
+ return title;
96
+ }),
75
97
 
76
- title: async () => {
77
- const t = await client.getTitle(tabId);
78
- logger.info(`[${name}] title: ${t}`);
79
- return t;
80
- },
81
98
  url: () => _currentUrl,
82
- content: () => client.content(tabId),
99
+ content: () => withTab(t => client.content(t)),
83
100
 
84
101
  wait: (ms: number) => {
85
102
  const actual = humanMode ? ms + Math.floor(Math.random() * 600) - 300 : ms;
86
103
  return new Promise<void>(r => setTimeout(r, Math.max(0, actual)));
87
104
  },
88
105
 
89
- waitForSelector: (selector: string, timeout = 30000) => {
90
- logger.debug(`[${name}] waitForSelector: ${selector}`);
91
- return client.waitForSelector(selector, timeout, tabId);
92
- },
93
- waitForVisible: (selector: string, timeout = 30000) => client.waitForSelector(selector, timeout, tabId),
94
- waitForResponse: (pattern: string, timeout = 30000) => client.waitForResponse(pattern, timeout, tabId),
106
+ waitForSelector: (selector: string, timeout = 30000) =>
107
+ withTab(t => {
108
+ logger.debug(`[${name}] waitForSelector: ${selector}`);
109
+ return client.waitForSelector(selector, timeout, t);
110
+ }),
111
+
112
+ waitForVisible: (selector: string, timeout = 30000) =>
113
+ withTab(t => client.waitForSelector(selector, timeout, t)),
114
+
115
+ waitForResponse: (pattern: string, timeout = 30000) =>
116
+ withTab(t => client.waitForResponse(pattern, timeout, t)),
95
117
 
96
- // ── Init Script ────────────────────────────────────────────────────────────
118
+ // ── Init Script ───────────────────────────────────────────────────────────
97
119
  addInitScript: async (js: string | (() => void)) => {
98
- const code = typeof js === 'function' ? `(${js.toString()})();` : js;
99
- await client.addInitScript(code, tabId);
120
+ const code = typeof js === "function" ? `(${js.toString()})();` : js;
121
+ await withTab(t => client.addInitScript(code, t));
100
122
  logger.success(`[${name}] init script added`);
101
123
  return site;
102
124
  },
103
125
 
104
- // ── Event emitter ──────────────────────────────────────────────────────────
105
- // Usage: site.on('navigate', url => console.log('went to', url))
106
- // Returns unsubscribe function
126
+ // ── Events ────────────────────────────────────────────────────────────────
107
127
  on: (event: string, handler: (data: any) => void): (() => void) => {
108
128
  if (!_eventListeners.has(event)) _eventListeners.set(event, new Set());
109
129
  _eventListeners.get(event)!.add(handler);
@@ -118,217 +138,199 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
118
138
  _eventListeners.get(event)?.delete(handler);
119
139
  },
120
140
 
121
- // ── Interactions ───────────────────────────────────────────────────────────
141
+ // ── Interactions ──────────────────────────────────────────────────────────
122
142
  click: (selector: string, opts?: { retries?: number; timeout?: number }) =>
123
143
  withErrScreen(() =>
124
- retry(name, async () => {
125
- if (humanMode) await randomDelay(80, 220);
126
- await client.waitForSelector(selector, opts?.timeout ?? 15000, tabId);
127
- const ok = await client.click(selector, tabId);
128
- if (!ok) throw new Error(`click failed: ${selector}`);
129
- logger.success(`[${name}] clicked: ${selector}`);
130
- return ok;
131
- }, opts?.retries ?? 2),
144
+ withTab(t =>
145
+ retry(name, async () => {
146
+ if (humanMode) await randomDelay(80, 220);
147
+ await client.waitForSelector(selector, opts?.timeout ?? 15000, t);
148
+ const ok = await client.click(selector, t);
149
+ if (!ok) throw new Error(`click failed: ${selector}`);
150
+ logger.success(`[${name}] clicked: ${selector}`);
151
+ return ok;
152
+ }, opts?.retries ?? 2)
153
+ ),
132
154
  `click(${selector})`
133
155
  ),
134
156
 
135
157
  doubleClick: (selector: string) =>
136
- withErrScreen(async () => {
137
- if (humanMode) await randomDelay(80, 200);
138
- return client.doubleClick(selector, tabId);
139
- }, `dblclick(${selector})`),
158
+ withErrScreen(() =>
159
+ withTab(async t => {
160
+ if (humanMode) await randomDelay(80, 200);
161
+ return client.doubleClick(selector, t);
162
+ }),
163
+ `dblclick(${selector})`
164
+ ),
140
165
 
141
166
  hover: (selector: string) =>
142
- withErrScreen(async () => {
143
- if (humanMode) await randomDelay(50, 150);
144
- return client.hover(selector, tabId);
145
- }, `hover(${selector})`),
146
-
147
- type: async (selector: string, text: string, opts?: { delay?: number; retries?: number; fact?: boolean; wpm?: number }) =>
148
- withErrScreen(async () => {
149
- await client.waitForSelector(selector, 15000, tabId);
150
- if (humanMode && !opts?.fact) {
151
- const seq = humanTypeSequence(text);
152
- let current = "";
153
- for (const action of seq) {
154
- if (action === "BACKSPACE") current = current.slice(0, -1);
155
- else current += action;
156
- await client.type(selector, current, tabId);
157
- const wpm = opts?.wpm ?? 120;
158
- const msPerChar = Math.round(60000 / (wpm * 5));
159
- await randomDelay(msPerChar * 0.5, msPerChar * 1.8);
160
- }
161
- } else if (opts?.delay) {
162
- for (const ch of text) {
163
- await client.type(selector, ch, tabId);
164
- await new Promise(r => setTimeout(r, opts.delay));
167
+ withErrScreen(() =>
168
+ withTab(async t => {
169
+ if (humanMode) await randomDelay(50, 150);
170
+ return client.hover(selector, t);
171
+ }),
172
+ `hover(${selector})`
173
+ ),
174
+
175
+ type: (selector: string, text: string, opts?: { delay?: number; retries?: number; fact?: boolean; wpm?: number }) =>
176
+ withErrScreen(() =>
177
+ withTab(async t => {
178
+ await client.waitForSelector(selector, 15000, t);
179
+ if (humanMode && !opts?.fact) {
180
+ const seq = humanTypeSequence(text);
181
+ let current = "";
182
+ for (const action of seq) {
183
+ if (action === "BACKSPACE") current = current.slice(0, -1);
184
+ else current += action;
185
+ await client.type(selector, current, t);
186
+ const wpm = opts?.wpm ?? 120;
187
+ const msPerChar = Math.round(60000 / (wpm * 5));
188
+ await randomDelay(msPerChar * 0.5, msPerChar * 1.8);
189
+ }
190
+ } else if (opts?.delay) {
191
+ for (const ch of text) {
192
+ await client.type(selector, ch, t);
193
+ await new Promise(r => setTimeout(r, opts.delay));
194
+ }
195
+ } else {
196
+ await client.type(selector, text, t);
165
197
  }
166
- } else {
167
- await client.type(selector, text, tabId);
168
- }
169
- logger.success(`[${name}] typed into: ${selector}`);
170
- return true;
171
- }, `type(${selector})`),
198
+ logger.success(`[${name}] typed into: ${selector}`);
199
+ return true;
200
+ }),
201
+ `type(${selector})`
202
+ ),
203
+
204
+ select: (selector: string, value: string) => withTab(t => client.select(selector, value, t)),
172
205
 
173
- select: (selector: string, value: string) => client.select(selector, value, tabId),
174
206
  evaluate: (js: string | (() => any), ...args: any[]) => {
175
- const code = typeof js === "function" ? `(${js.toString()})(${args.map(a => JSON.stringify(a)).join(",")})` : js;
176
- return client.evaluate(code, tabId);
207
+ const code = typeof js === "function"
208
+ ? `(${js.toString()})(${args.map(a => JSON.stringify(a)).join(",")})`
209
+ : js;
210
+ return withTab(t => client.evaluate(code, t));
177
211
  },
178
212
 
179
213
  keyboard: {
180
- press: (key: string) => client.keyPress(key, tabId),
181
- combo: (combo: string) => client.keyCombo(combo, tabId),
214
+ press: (key: string) => withTab(t => client.keyPress(key, t)),
215
+ combo: (combo: string) => withTab(t => client.keyCombo(combo, t)),
182
216
  },
183
217
 
184
218
  mouse: {
185
- move: (x: number, y: number) => client.mouseMove(x, y, tabId),
186
- drag: (from: { x: number; y: number }, to: { x: number; y: number }) => client.mouseDrag(from, to, tabId),
219
+ move: (x: number, y: number) => withTab(t => client.mouseMove(x, y, t)),
220
+ drag: (from: { x: number; y: number }, to: { x: number; y: number }) =>
221
+ withTab(t => client.mouseDrag(from, to, t)),
187
222
  },
188
223
 
189
224
  scroll: {
190
- to: (selector: string) => client.scrollTo(selector, tabId),
191
- by: (px: number) => {
192
- if (humanMode) {
193
- const steps = Math.ceil(Math.abs(px) / 120);
194
- const chunk = px / steps;
195
- return (async () => {
225
+ to: (selector: string) => withTab(t => client.scrollTo(selector, t)),
226
+ by: (px: number) => withTab(async t => {
227
+ if (humanMode) {
228
+ const steps = Math.ceil(Math.abs(px) / 120);
229
+ const chunk = px / steps;
196
230
  for (let i = 0; i < steps; i++) {
197
- await client.scrollBy(chunk, tabId);
231
+ await client.scrollBy(chunk, t);
198
232
  await randomDelay(30, 80);
199
233
  }
200
- })();
201
- }
202
- return client.scrollBy(px, tabId);
203
- },
234
+ } else {
235
+ await client.scrollBy(px, t);
236
+ }
237
+ }) as Promise<void>,
204
238
  },
205
239
 
206
- // ── Fetch ──────────────────────────────────────────────────────────────────
207
- fetchText: (selector: string) => client.fetchText(selector, tabId),
208
- fetchLinks: async (selector: string) => {
209
- const links = await client.fetchLinks(selector, tabId);
240
+ // ── Fetch ─────────────────────────────────────────────────────────────────
241
+ fetchText: (selector: string) => withTab(t => client.fetchText(selector, t)),
242
+
243
+ fetchLinks: async (selector: string) => {
244
+ const links = await withTab(t => client.fetchLinks(selector, t));
210
245
  logger.info(`[${name}] fetchLinks(${selector}): ${links.length}`);
211
246
  return links;
212
247
  },
248
+
213
249
  fetchImages: async (selector: string) => {
214
- const imgs = await client.fetchImages(selector, tabId);
250
+ const imgs = await withTab(t => client.fetchImages(selector, t));
215
251
  logger.info(`[${name}] fetchImages(${selector}): ${imgs.length}`);
216
252
  return imgs;
217
253
  },
218
254
 
219
255
  search: {
220
- css: (query: string) => client.searchCss(query, tabId),
221
- id: (query: string) => client.searchId(query, tabId),
256
+ css: (query: string) => withTab(t => client.searchCss(query, t)),
257
+ id: (query: string) => withTab(t => client.searchId(query, t)),
222
258
  },
223
259
 
224
- // ── Screenshot / PDF ───────────────────────────────────────────────────────
260
+ // ── Screenshot / PDF ──────────────────────────────────────────────────────
225
261
  screenshot: async (filePath?: string) => {
226
- const r = await client.screenshot(filePath, tabId);
262
+ const r = await withTab(t => client.screenshot(filePath, t));
227
263
  logger.success(`[${name}] screenshot → ${filePath ?? "base64"}`);
228
264
  return r;
229
265
  },
266
+
230
267
  pdf: async (filePath?: string) => {
231
- const r = await client.pdf(filePath, tabId);
268
+ const r = await withTab(t => client.pdf(filePath, t));
232
269
  logger.success(`[${name}] pdf → ${filePath ?? "base64"}`);
233
270
  return r;
234
271
  },
235
272
 
236
- blockImages: async () => { await client.blockImages(tabId); logger.info(`[${name}] images blocked`); },
237
- unblockImages: async () => { await client.unblockImages(tabId); logger.info(`[${name}] images unblocked`); },
273
+ blockImages: () => withTab(async t => { await client.blockImages(t); logger.info(`[${name}] images blocked`); }),
274
+ unblockImages: () => withTab(async t => { await client.unblockImages(t); logger.info(`[${name}] images unblocked`); }),
238
275
 
239
- // ── Cookies ────────────────────────────────────────────────────────────────
276
+ // ── Cookies ───────────────────────────────────────────────────────────────
240
277
  cookies: {
241
278
  set: async (cookieName: string, value: string, domain: string, path = "/") => {
242
- await client.setCookie(cookieName, value, domain, path, tabId);
279
+ await withTab(t => client.setCookie(cookieName, value, domain, path, t));
243
280
  logger.info(`[${name}] cookie set: ${cookieName} @ ${domain}`);
244
281
  },
245
- get: (cookieName: string) => client.getCookie(cookieName, tabId),
282
+ get: (cookieName: string) => withTab(t => client.getCookie(cookieName, t)),
246
283
  delete: async (cookieName: string) => {
247
- await client.deleteCookie(cookieName, tabId);
284
+ await withTab(t => client.deleteCookie(cookieName, t));
248
285
  logger.info(`[${name}] cookie deleted: ${cookieName}`);
249
286
  },
250
- list: () => client.listCookies(tabId),
287
+ list: () => withTab(t => client.listCookies(t)),
251
288
  },
252
289
 
253
- // ── Interception ───────────────────────────────────────────────────────────
290
+ // ── Interception ──────────────────────────────────────────────────────────
254
291
  intercept: {
255
292
  block: async (pattern: string) => {
256
- await client.addInterceptRule("block", pattern, {}, tabId);
293
+ await withTab(t => client.addInterceptRule("block", pattern, {}, t));
257
294
  logger.info(`[${name}] intercept block: ${pattern}`);
258
295
  },
259
296
 
260
297
  redirect: async (pattern: string, redirectUrl: string) => {
261
- await client.addInterceptRule("redirect", pattern, { redirectUrl }, tabId);
298
+ await withTab(t => client.addInterceptRule("redirect", pattern, { redirectUrl }, t));
262
299
  logger.info(`[${name}] intercept redirect: ${pattern} → ${redirectUrl}`);
263
300
  },
264
301
 
265
302
  headers: async (pattern: string, headers: Record<string, string>) => {
266
- await client.addInterceptRule("modifyHeaders", pattern, { headers }, tabId);
303
+ await withTab(t => client.addInterceptRule("modifyHeaders", pattern, { headers }, t));
267
304
  logger.info(`[${name}] intercept modifyHeaders: ${pattern}`);
268
305
  },
269
306
 
270
- // ── NEW: intercept.respond ──────────────────────────────────────────────
271
- // Intercepts matching requests and returns a fake response — request never
272
- // leaves the browser. Works for both fetch and XHR via JS injection.
273
- //
274
- // Usage:
275
- // await site.intercept.respond('/api/prices', (req) => ({
276
- // status: 200,
277
- // contentType: 'application/json',
278
- // body: JSON.stringify({ price: 99 })
279
- // }))
280
- //
281
- // // Static shorthand:
282
- // await site.intercept.respond('/api/prices', {
283
- // status: 200, contentType: 'application/json', body: '{"price":99}'
284
- // })
285
307
  respond: async (
286
308
  pattern: string,
287
309
  handlerOrResponse:
288
310
  | { status?: number; contentType?: string; body: string }
289
311
  | ((req: { url: string; method: string }) => { status?: number; contentType?: string; body: string })
290
312
  ) => {
291
- // Static response — just inject the JS rule directly
292
313
  const isStatic = typeof handlerOrResponse === "object";
293
- const response = isStatic
294
- ? handlerOrResponse
295
- : { status: 200, contentType: "application/json", body: "" };
296
314
 
297
315
  if (!isStatic) {
298
- // Dynamic: expose a function, call it from the injected script
299
316
  const fnName = `__piggy_respond_${name}_${++_modifyRuleCounter}__`;
300
-
301
317
  await client.exposeFunction(fnName, async (req: { url: string; method: string }) => {
302
318
  try {
303
319
  const result = (handlerOrResponse as Function)(req);
304
- return {
305
- success: true,
306
- result: {
307
- status: result.status ?? 200,
308
- contentType: result.contentType ?? "application/json",
309
- body: result.body ?? "",
310
- }
311
- };
320
+ return { success: true, result: { status: result.status ?? 200, contentType: result.contentType ?? "application/json", body: result.body ?? "" } };
312
321
  } catch (e: any) {
313
322
  return { success: false, error: e.message };
314
323
  }
315
324
  }, tabId);
316
325
 
317
- // Inject a script that calls the exposed function instead of static body
318
326
  const dynamicScript = `
319
327
  (function() {
320
328
  'use strict';
321
329
  if (!window.__PIGGY_DYNAMIC_RESPOND__) window.__PIGGY_DYNAMIC_RESPOND__ = [];
322
330
  window.__PIGGY_DYNAMIC_RESPOND__.push({ pattern: ${JSON.stringify(pattern)}, fn: ${JSON.stringify(fnName)} });
323
-
324
- function matchUrl(url, pattern) {
325
- try { return url.includes(pattern) || new RegExp(pattern).test(url); }
326
- catch { return url.includes(pattern); }
327
- }
328
-
331
+ function matchUrl(url, pattern) { try { return url.includes(pattern) || new RegExp(pattern).test(url); } catch { return url.includes(pattern); } }
329
332
  if (window.__PIGGY_DYN_INSTALLED__) return;
330
333
  window.__PIGGY_DYN_INSTALLED__ = true;
331
-
332
334
  const _origFetch = window.fetch;
333
335
  window.fetch = async function(input, init) {
334
336
  const url = typeof input === 'string' ? input : (input?.url ?? String(input));
@@ -336,95 +338,81 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
336
338
  const rules = window.__PIGGY_DYNAMIC_RESPOND__ || [];
337
339
  for (const rule of rules) {
338
340
  if (matchUrl(url, rule.pattern) && typeof window[rule.fn] === 'function') {
339
- try {
340
- const r = await window[rule.fn]({ url, method });
341
- return new Response(r.body ?? '', {
342
- status: r.status ?? 200,
343
- headers: { 'Content-Type': r.contentType ?? 'application/json' }
344
- });
345
- } catch { break; }
341
+ try { const r = await window[rule.fn]({ url, method }); return new Response(r.body ?? '', { status: r.status ?? 200, headers: { 'Content-Type': r.contentType ?? 'application/json' } }); } catch { break; }
346
342
  }
347
343
  }
348
344
  return _origFetch.apply(this, arguments);
349
345
  };
350
346
  })();`;
351
- await client.addInitScript(dynamicScript, tabId);
352
- await client.evaluate(dynamicScript, tabId);
347
+ await withTab(async t => {
348
+ await client.addInitScript(dynamicScript, t);
349
+ await client.evaluate(dynamicScript, t);
350
+ });
353
351
  logger.success(`[${name}] intercept.respond (dynamic): ${pattern}`);
354
352
  return site;
355
353
  }
356
354
 
357
- // Static path: inject the JS intercept rule
358
- const script = buildRespondScript(
359
- pattern,
360
- response.status ?? 200,
361
- response.contentType ?? "application/json",
362
- response.body
363
- );
364
- await client.addInitScript(script, tabId);
365
- await client.evaluate(script, tabId);
355
+ const response = handlerOrResponse;
356
+ const script = buildRespondScript(pattern, response.status ?? 200, response.contentType ?? "application/json", response.body);
357
+ await withTab(async t => {
358
+ await client.addInitScript(script, t);
359
+ await client.evaluate(script, t);
360
+ });
366
361
  logger.success(`[${name}] intercept.respond (static): ${pattern} → ${response.status ?? 200}`);
367
362
  return site;
368
363
  },
369
364
 
370
- // ── NEW: intercept.modifyResponse ───────────────────────────────────────
371
- // Lets the request hit the network, then calls your handler with the
372
- // response. Return { body?, status?, headers? } to modify, or {} to
373
- // pass through unchanged.
374
- //
375
- // Usage:
376
- // await site.intercept.modifyResponse('/api/feed', async ({ body, status }) => {
377
- // const data = JSON.parse(body)
378
- // data.items = data.items.slice(0, 5)
379
- // return { body: JSON.stringify(data) }
380
- // })
381
365
  modifyResponse: async (
382
366
  pattern: string,
383
367
  handler: (response: { body: string; status: number; headers: Record<string, string> }) =>
384
368
  Promise<{ body?: string; status?: number; headers?: Record<string, string> } | void> | void
385
369
  ) => {
386
370
  const fnName = `__piggy_modres_${name}_${++_modifyRuleCounter}__`;
387
-
388
- await client.exposeFunction(fnName, async (response: { body: string; status: number; headers: Record<string, string> }) => {
389
- try {
390
- const mod = await handler(response);
391
- return { success: true, result: mod ?? {} };
392
- } catch (e: any) {
393
- return { success: false, error: e.message };
394
- }
371
+ await client.exposeFunction(fnName, async (response: any) => {
372
+ try { const mod = await handler(response); return { success: true, result: mod ?? {} }; }
373
+ catch (e: any) { return { success: false, error: e.message }; }
395
374
  }, tabId);
396
375
 
397
376
  const script = buildModifyResponseScript(pattern, fnName);
398
- await client.addInitScript(script, tabId);
399
- await client.evaluate(script, tabId);
377
+ await withTab(async t => {
378
+ await client.addInitScript(script, t);
379
+ await client.evaluate(script, t);
380
+ });
400
381
  logger.success(`[${name}] intercept.modifyResponse: ${pattern}`);
401
382
  return site;
402
383
  },
403
384
 
404
385
  clear: async () => {
405
- await client.clearInterceptRules(tabId);
386
+ await withTab(t => client.clearInterceptRules(t));
406
387
  logger.info(`[${name}] intercept rules cleared`);
407
388
  },
408
389
  },
409
390
 
410
- // ── Network capture ────────────────────────────────────────────────────────
391
+ // ── Network capture ───────────────────────────────────────────────────────
411
392
  capture: {
412
- start: async () => { await client.captureStart(tabId); logger.info(`[${name}] capture started`); },
413
- stop: async () => { await client.captureStop(tabId); logger.info(`[${name}] capture stopped`); },
414
- requests: () => client.captureRequests(tabId),
415
- ws: () => client.captureWs(tabId),
416
- cookies: () => client.captureCookies(tabId),
417
- storage: () => client.captureStorage(tabId),
418
- clear: async () => { await client.captureClear(tabId); logger.info(`[${name}] capture cleared`); },
393
+ start: () => withTab(async t => { await client.captureStart(t); logger.info(`[${name}] capture started`); }),
394
+ stop: () => withTab(async t => { await client.captureStop(t); logger.info(`[${name}] capture stopped`); }),
395
+ requests: () => withTab(t => client.captureRequests(t)),
396
+ ws: () => withTab(t => client.captureWs(t)),
397
+ cookies: () => withTab(t => client.captureCookies(t)),
398
+ storage: () => withTab(t => client.captureStorage(t)),
399
+ clear: () => withTab(async t => { await client.captureClear(t); logger.info(`[${name}] capture cleared`); }),
419
400
  },
420
401
 
421
- // ── Session ────────────────────────────────────────────────────────────────
402
+ // ── Session ───────────────────────────────────────────────────────────────
422
403
  session: {
423
- export: async () => { const data = await client.sessionExport(tabId); logger.success(`[${name}] session exported`); return data; },
424
- import: async (data: any) => { await client.sessionImport(data, tabId); logger.success(`[${name}] session imported`); },
404
+ export: async () => {
405
+ const data = await withTab(t => client.sessionExport(t));
406
+ logger.success(`[${name}] session exported`);
407
+ return data;
408
+ },
409
+ import: async (data: any) => {
410
+ await withTab(t => client.sessionImport(data, t));
411
+ logger.success(`[${name}] session imported`);
412
+ },
425
413
  },
426
414
 
427
- // ── Expose Function ─────────────────────────────────────────────────────────
415
+ // ── Expose Function ───────────────────────────────────────────────────────
428
416
  exposeFunction: async (fnName: string, handler: (data: any) => Promise<any> | any) => {
429
417
  await client.exposeFunction(fnName, handler, tabId);
430
418
  logger.success(`[${name}] exposed function: ${fnName}`);
@@ -443,21 +431,42 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
443
431
  exposeAndInject: async (fnName: string, handler: (data: any) => Promise<any> | any, injectionJs: string | ((fnName: string) => string)) => {
444
432
  await client.exposeFunction(fnName, handler, tabId);
445
433
  const js = typeof injectionJs === "function" ? injectionJs(fnName) : injectionJs;
446
- await client.evaluate(js, tabId);
434
+ await withTab(t => client.evaluate(js, t));
447
435
  logger.success(`[${name}] exposed and injected: ${fnName}`);
448
436
  return site;
449
437
  },
450
438
 
451
- // ── Elysia API ─────────────────────────────────────────────────────────────
452
- api: (path: string, handler: RouteHandler, opts?: { ttl?: number; before?: BeforeMiddleware[]; method?: "GET" | "POST" | "PUT" | "DELETE" }) => {
439
+ // ── Store ─────────────────────────────────────────────────────────────────
440
+ store: async (
441
+ data: Record<string, any> | Record<string, any>[],
442
+ schemaName?: string
443
+ ) => {
444
+ const target = schemaName ?? name;
445
+ const result = await storeRecord(target, data);
446
+ logger.info(`[${name}] store → stored: ${result.stored}, skipped: ${result.skipped}`);
447
+ return result;
448
+ },
449
+
450
+ // ── Elysia API ────────────────────────────────────────────────────────────
451
+ api: (
452
+ path: string,
453
+ handler: RouteHandler,
454
+ opts?: {
455
+ ttl?: number;
456
+ before?: BeforeMiddleware[];
457
+ method?: "GET" | "POST" | "PUT" | "DELETE";
458
+ detail?: RouteDetail;
459
+ }
460
+ ) => {
453
461
  const key = `${name}:${path}`;
454
462
  if (routeRegistry.has(key)) { logger.warn(`[${name}] route ${path} already registered`); return site; }
455
463
  routeRegistry.set(key, {
456
464
  path,
457
- method: opts?.method ?? "GET",
465
+ method: opts?.method ?? "GET",
458
466
  handler,
459
- ttl: opts?.ttl ?? 360_000,
460
- before: opts?.before ?? [],
467
+ ttl: opts?.ttl ?? 360_000,
468
+ before: opts?.before ?? [],
469
+ detail: opts?.detail,
461
470
  });
462
471
  logger.info(`[${name}] api route: ${opts?.method ?? "GET"} /${name}${path}`);
463
472
  return site;
@@ -466,9 +475,11 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
466
475
  noclose: () => { keepAliveSites.add(name); logger.info(`[${name}] keep-alive`); return site; },
467
476
 
468
477
  close: async () => {
469
- _unsubNavigate(); // Clean up navigate listener
478
+ _unsubNavigate();
470
479
  keepAliveSites.delete(name);
471
- if (tabId !== "default") {
480
+ if (pool) {
481
+ await pool.close();
482
+ } else if (tabId !== "default") {
472
483
  await client.closeTab(tabId);
473
484
  logger.info(`[${name}] tab closed`);
474
485
  }
@@ -478,7 +489,13 @@ export function createSiteObject(name: string, registeredUrl: string, client: Pi
478
489
  return site;
479
490
  }
480
491
 
481
- export function createExposedAPI<T extends Record<string, (data: any) => any>>(site: any, apiName: string, handlers: T): Promise<void> {
492
+ export type SiteObject = ReturnType<typeof createSiteObject>;
493
+
494
+ export function createExposedAPI<T extends Record<string, (data: any) => any>>(
495
+ site: any,
496
+ apiName: string,
497
+ handlers: T
498
+ ): Promise<void> {
482
499
  const wrappedHandler = async (call: any) => {
483
500
  const { method, args } = call;
484
501
  const handler = handlers[method as keyof T];