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/dist/piggy/pool/index.d.ts +23 -0
- package/dist/piggy/pool/index.d.ts.map +1 -0
- package/dist/piggy/register/index.d.ts +3 -1
- package/dist/piggy/register/index.d.ts.map +1 -1
- package/dist/piggy/server/index.d.ts +22 -1
- package/dist/piggy/server/index.d.ts.map +1 -1
- package/dist/piggy/store/index.d.ts +22 -0
- package/dist/piggy/store/index.d.ts.map +1 -0
- package/dist/piggy.d.ts +6 -174
- package/dist/piggy.d.ts.map +1 -1
- package/dist/piggy.js +7391 -205
- package/dist/register/index.js +6291 -205
- package/dist/server/index.js +6252 -79
- package/package.json +3 -1
- package/piggy/pool/index.d.ts +12 -0
- package/piggy/pool/index.ts +75 -0
- package/piggy/register/index.ts +231 -214
- package/piggy/server/index.d.ts +51 -14
- package/piggy/server/index.ts +68 -15
- package/piggy/store/index.d.ts +26 -0
- package/piggy/store/index.ts +230 -0
- package/piggy.ts +95 -316
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
|
-
|
|
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
|
-
|
|
17
|
+
const piggy: any = {
|
|
18
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
279
19
|
|
|
280
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
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")
|
|
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
|
-
|
|
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)
|
|
327
|
-
_tabMode = m;
|
|
328
|
-
return piggy;
|
|
329
|
-
},
|
|
90
|
+
mode: (m: TabMode) => { _tabMode = m; return piggy; },
|
|
330
91
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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")
|
|
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
|
-
|
|
350
|
-
|
|
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
|
-
|
|
355
|
-
|
|
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
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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[])
|
|
378
|
-
new Proxy({} as
|
|
379
|
-
get: (
|
|
380
|
-
async (...args:
|
|
381
|
-
const results = await Promise.all(
|
|
382
|
-
|
|
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
|
-
|
|
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 };
|