app-manager-edge-worker 1.0.8 → 1.0.9

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/src/client.js CHANGED
@@ -1,300 +1,300 @@
1
- import { io } from "socket.io-client";
2
- import os from "os";
3
- import path from "path";
4
- import axios from "axios";
5
- import { execSync } from "child_process";
6
- import localBrowserManager from "./browser.js";
7
- import { handleChromiumScrape, handleHttpScrape } from "./handlers/scrapeHandler.js";
8
- import { handlePreviewPage, handleInspectElement } from "./handlers/previewHandler.js";
9
- import { handleSendEmail } from "./handlers/mailHandler.js";
10
- import { handleAiSmartRepair } from "./handlers/aiRepairHandler.js";
11
- import { printBanner, logInfo, logSuccess, logWarn, logError, logJob } from "./banner.js";
12
-
13
- export async function startWorkerClient(config) {
14
- const { workerKey, serverUrl, concurrency, workerName, keepAliveMinutes = 7 } = config;
15
- const isTermux = localBrowserManager.isTermux();
16
-
17
- printBanner({
18
- workerName,
19
- serverUrl,
20
- concurrency,
21
- platform: `${os.platform()} ${os.arch()}`,
22
- isTermux,
23
- keepAliveMinutes,
24
- });
25
-
26
- if (isTermux) {
27
- try {
28
- execSync("termux-wake-lock", { stdio: "ignore" });
29
- logSuccess("[Android Termux] Wake lock acquired to keep CPU awake in background.");
30
- } catch {
31
- // termux-tools might not be installed yet
32
- }
33
- }
34
-
35
- logInfo("Checking Chromium browser availability...");
36
- let chromiumExecutable = null;
37
- try {
38
- chromiumExecutable = await localBrowserManager.ensureChromiumExecutable();
39
- logSuccess(`Chromium ready: ${chromiumExecutable}`);
40
- } catch (browserErr) {
41
- logWarn(`Chromium initialization warning: ${browserErr.message}`);
42
- }
43
-
44
- logInfo("Connecting to Apps Manager cluster...");
45
-
46
- const socket = io(`${serverUrl}/workers`, {
47
- transports: ["websocket", "polling"],
48
- reconnection: true,
49
- reconnectionAttempts: Infinity,
50
- reconnectionDelay: 1000,
51
- reconnectionDelayMax: 5000,
52
- auth: {
53
- workerKey,
54
- metadata: {
55
- hostname: workerName || os.hostname(),
56
- platform: os.platform(),
57
- arch: os.arch(),
58
- osVersion: os.release(),
59
- nodeVersion: process.version,
60
- chromiumVersion: chromiumExecutable ? path.basename(chromiumExecutable) : "",
61
- chromiumPath: chromiumExecutable || "",
62
- isTermux,
63
- },
64
- },
65
- });
66
-
67
- const CHUNK_SIZE = 256 * 1024; // 256 KB chunks to prevent any WebSocket frame buffer overflow
68
-
69
- const sendJobResult = (payload) => {
70
- let payloadStr = "";
71
- try {
72
- payloadStr = JSON.stringify(payload);
73
- } catch {
74
- socket.emit("job:result", payload);
75
- return;
76
- }
77
-
78
- // Small payload: send directly
79
- if (payloadStr.length <= CHUNK_SIZE) {
80
- socket.emit("job:result", payload);
81
- return;
82
- }
83
-
84
- // Large payload (e.g. screenshot or large scrape): stream in chunks
85
- const totalChunks = Math.ceil(payloadStr.length / CHUNK_SIZE);
86
- const jobId = payload.jobId;
87
-
88
- socket.emit("job:result:stream:start", {
89
- jobId,
90
- totalChunks,
91
- totalLength: payloadStr.length,
92
- });
93
-
94
- for (let i = 0; i < totalChunks; i++) {
95
- const chunk = payloadStr.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
96
- socket.emit("job:result:stream:chunk", {
97
- jobId,
98
- index: i,
99
- chunk,
100
- });
101
- }
102
-
103
- socket.emit("job:result:stream:end", { jobId });
104
- };
105
-
106
- let activeJobs = 0;
107
- let activeConcurrency = concurrency;
108
-
109
- socket.on("connect", () => {
110
- logSuccess(`Connected to cluster! (Socket ID: ${socket.id})`);
111
- logInfo(`Ready to accept scraping and visual preview jobs (Concurrency: ${activeConcurrency}).\n`);
112
- });
113
-
114
- socket.on("worker:registered", (data) => {
115
- if (data?.concurrencyLimit && typeof data.concurrencyLimit === "number") {
116
- activeConcurrency = data.concurrencyLimit;
117
- logInfo(`[Cluster Config] Worker registered as '${data.name || data.workerId}'. Active concurrency: ${activeConcurrency} concurrent tabs.`);
118
- }
119
- });
120
-
121
- socket.on("connect_error", (err) => {
122
- if (err.message && err.message.includes("Authentication failed")) {
123
- logError(`Authentication Failed: ${err.message}`);
124
- logWarn("Please verify your worker key or run with '--reset' to enter a new key.");
125
- } else {
126
- logWarn(`Connection failed (${err.message}). Retrying in a few seconds...`);
127
- }
128
- });
129
-
130
- socket.on("disconnect", (reason) => {
131
- logWarn(`Disconnected from cluster: ${reason}`);
132
- });
133
-
134
- // Handle incoming job dispatch from server
135
- socket.on("job:dispatch", async (job) => {
136
- const jobId = job.jobId;
137
- const jobType = job.jobType || job.type;
138
- const payload = job.payload || {};
139
- const targetUrl = jobType === "SEND_EMAIL"
140
- ? (payload.mailOptions?.to ? `to: ${payload.mailOptions.to}` : "email")
141
- : (payload.url || "(no url)");
142
-
143
- logJob(jobType, targetUrl);
144
- activeJobs++;
145
-
146
- const startTime = Date.now();
147
-
148
- try {
149
- let resultData = null;
150
-
151
- if (jobType === "SCRAPE_CHROMIUM") {
152
- resultData = await handleChromiumScrape(payload);
153
- } else if (jobType === "SCRAPE_HTTP") {
154
- resultData = await handleHttpScrape(payload);
155
- } else if (jobType === "PREVIEW_PAGE") {
156
- resultData = await handlePreviewPage(payload);
157
- } else if (jobType === "INSPECT_ELEMENT") {
158
- resultData = await handleInspectElement(payload);
159
- } else if (jobType === "SEND_EMAIL") {
160
- resultData = await handleSendEmail(payload);
161
- } else if (jobType === "AI_SMART_REPAIR") {
162
- resultData = await handleAiSmartRepair(payload, config);
163
- } else {
164
- throw new Error(`Unsupported job type: ${jobType}`);
165
- }
166
-
167
- const durationMs = Date.now() - startTime;
168
- logSuccess(`Completed ${jobType} for ${targetUrl} in ${durationMs}ms`);
169
-
170
- sendJobResult({
171
- jobId,
172
- success: true,
173
- data: resultData,
174
- durationMs,
175
- });
176
- } catch (err) {
177
- const durationMs = Date.now() - startTime;
178
- logError(`Failed ${jobType} for ${targetUrl}: ${err.message}`);
179
-
180
- sendJobResult({
181
- jobId,
182
- success: false,
183
- error: {
184
- message: err.message,
185
- code: err.code || "JOB_EXECUTION_ERROR",
186
- },
187
- durationMs,
188
- });
189
- } finally {
190
- activeJobs = Math.max(0, activeJobs - 1);
191
- }
192
- });
193
-
194
- // Handle cluster heartbeat ping
195
- socket.on("ping", () => {
196
- socket.emit("pong", {
197
- activeJobs,
198
- memory: process.memoryUsage(),
199
- uptime: process.uptime(),
200
- });
201
- });
202
-
203
- // Render Anti-Sleep Keep-Alive Ping
204
- let keepAliveTimer = null;
205
- let initialKeepAliveTimer = null;
206
-
207
- const clearKeepAliveTimers = () => {
208
- if (keepAliveTimer) {
209
- clearInterval(keepAliveTimer);
210
- keepAliveTimer = null;
211
- }
212
- if (initialKeepAliveTimer) {
213
- clearTimeout(initialKeepAliveTimer);
214
- initialKeepAliveTimer = null;
215
- }
216
- };
217
-
218
- const pingKeepAlive = async () => {
219
- try {
220
- // Try the dedicated keep-alive endpoint first
221
- const endpoint = `${serverUrl}/api/workers/keep-alive`;
222
- const res = await axios.get(endpoint, {
223
- timeout: 15000,
224
- headers: {
225
- "x-worker-key": workerKey || "",
226
- "user-agent": "AppsManagerEdgeWorker/1.0.4",
227
- },
228
- });
229
- logInfo(`[Keep-Alive] Server awake ping OK (${res.status}) - Render instance active`);
230
- } catch (err) {
231
- // Fallback to /health
232
- try {
233
- const fallbackUrl = `${serverUrl}/health`;
234
- await axios.get(fallbackUrl, {
235
- timeout: 10000,
236
- headers: {
237
- "user-agent": "AppsManagerEdgeWorker/1.0.4",
238
- },
239
- });
240
- logInfo(`[Keep-Alive] Fallback health ping OK - Render instance active`);
241
- } catch (fallbackErr) {
242
- logWarn(`[Keep-Alive] Server awake ping warning (${err.message}) - will retry next cycle`);
243
- }
244
- }
245
- };
246
-
247
- const startKeepAlive = () => {
248
- if (keepAliveMinutes <= 0) return;
249
- clearKeepAliveTimers();
250
-
251
- // Send initial ping 15s after startup to warm up/verify
252
- initialKeepAliveTimer = setTimeout(pingKeepAlive, 15000);
253
- if (initialKeepAliveTimer.unref) initialKeepAliveTimer.unref();
254
-
255
- const intervalMs = Math.max(1, keepAliveMinutes) * 60 * 1000;
256
- keepAliveTimer = setInterval(pingKeepAlive, intervalMs);
257
- if (keepAliveTimer.unref) keepAliveTimer.unref();
258
- };
259
-
260
- if (keepAliveMinutes > 0) {
261
- startKeepAlive();
262
- }
263
-
264
- // Clean up keep-alive timers on socket disconnect
265
- socket.on("disconnect", (reason) => {
266
- logWarn(`Disconnected from cluster: ${reason}`);
267
- clearKeepAliveTimers();
268
- });
269
-
270
- socket.on("connect", () => {
271
- if (keepAliveMinutes > 0 && !keepAliveTimer) {
272
- startKeepAlive();
273
- }
274
- });
275
-
276
- // Provide programmatic close for automated test harnesses
277
- socket.closeClient = async () => {
278
- clearKeepAliveTimers();
279
- if (isTermux) {
280
- try {
281
- execSync("termux-wake-unlock", { stdio: "ignore" });
282
- } catch {}
283
- }
284
- socket.disconnect();
285
- await localBrowserManager.close();
286
- };
287
-
288
- // Graceful shutdown handling
289
- const shutdown = async () => {
290
- logInfo("\nGracefully shutting down edge worker...");
291
- await socket.closeClient();
292
- logSuccess("Edge worker stopped cleanly.");
293
- process.exit(0);
294
- };
295
-
296
- process.on("SIGINT", shutdown);
297
- process.on("SIGTERM", shutdown);
298
-
299
- return socket;
300
- }
1
+ import { io } from "socket.io-client";
2
+ import os from "os";
3
+ import path from "path";
4
+ import axios from "axios";
5
+ import { execSync } from "child_process";
6
+ import localBrowserManager from "./browser.js";
7
+ import { handleChromiumScrape, handleHttpScrape } from "./handlers/scrapeHandler.js";
8
+ import { handlePreviewPage, handleInspectElement } from "./handlers/previewHandler.js";
9
+ import { handleSendEmail } from "./handlers/mailHandler.js";
10
+ import { handleAiSmartRepair } from "./handlers/aiRepairHandler.js";
11
+ import { printBanner, logInfo, logSuccess, logWarn, logError, logJob } from "./banner.js";
12
+
13
+ export async function startWorkerClient(config) {
14
+ const { workerKey, serverUrl, concurrency, workerName, keepAliveMinutes = 7 } = config;
15
+ const isTermux = localBrowserManager.isTermux();
16
+
17
+ printBanner({
18
+ workerName,
19
+ serverUrl,
20
+ concurrency,
21
+ platform: `${os.platform()} ${os.arch()}`,
22
+ isTermux,
23
+ keepAliveMinutes,
24
+ });
25
+
26
+ if (isTermux) {
27
+ try {
28
+ execSync("termux-wake-lock", { stdio: "ignore" });
29
+ logSuccess("[Android Termux] Wake lock acquired to keep CPU awake in background.");
30
+ } catch {
31
+ // termux-tools might not be installed yet
32
+ }
33
+ }
34
+
35
+ logInfo("Checking Chromium browser availability...");
36
+ let chromiumExecutable = null;
37
+ try {
38
+ chromiumExecutable = await localBrowserManager.ensureChromiumExecutable();
39
+ logSuccess(`Chromium ready: ${chromiumExecutable}`);
40
+ } catch (browserErr) {
41
+ logWarn(`Chromium initialization warning: ${browserErr.message}`);
42
+ }
43
+
44
+ logInfo("Connecting to Apps Manager cluster...");
45
+
46
+ const socket = io(`${serverUrl}/workers`, {
47
+ transports: ["websocket", "polling"],
48
+ reconnection: true,
49
+ reconnectionAttempts: Infinity,
50
+ reconnectionDelay: 1000,
51
+ reconnectionDelayMax: 5000,
52
+ auth: {
53
+ workerKey,
54
+ metadata: {
55
+ hostname: workerName || os.hostname(),
56
+ platform: os.platform(),
57
+ arch: os.arch(),
58
+ osVersion: os.release(),
59
+ nodeVersion: process.version,
60
+ chromiumVersion: chromiumExecutable ? path.basename(chromiumExecutable) : "",
61
+ chromiumPath: chromiumExecutable || "",
62
+ isTermux,
63
+ },
64
+ },
65
+ });
66
+
67
+ const CHUNK_SIZE = 256 * 1024; // 256 KB chunks to prevent any WebSocket frame buffer overflow
68
+
69
+ const sendJobResult = (payload) => {
70
+ let payloadStr = "";
71
+ try {
72
+ payloadStr = JSON.stringify(payload);
73
+ } catch {
74
+ socket.emit("job:result", payload);
75
+ return;
76
+ }
77
+
78
+ // Small payload: send directly
79
+ if (payloadStr.length <= CHUNK_SIZE) {
80
+ socket.emit("job:result", payload);
81
+ return;
82
+ }
83
+
84
+ // Large payload (e.g. screenshot or large scrape): stream in chunks
85
+ const totalChunks = Math.ceil(payloadStr.length / CHUNK_SIZE);
86
+ const jobId = payload.jobId;
87
+
88
+ socket.emit("job:result:stream:start", {
89
+ jobId,
90
+ totalChunks,
91
+ totalLength: payloadStr.length,
92
+ });
93
+
94
+ for (let i = 0; i < totalChunks; i++) {
95
+ const chunk = payloadStr.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
96
+ socket.emit("job:result:stream:chunk", {
97
+ jobId,
98
+ index: i,
99
+ chunk,
100
+ });
101
+ }
102
+
103
+ socket.emit("job:result:stream:end", { jobId });
104
+ };
105
+
106
+ let activeJobs = 0;
107
+ let activeConcurrency = concurrency;
108
+
109
+ socket.on("connect", () => {
110
+ logSuccess(`Connected to cluster! (Socket ID: ${socket.id})`);
111
+ logInfo(`Ready to accept scraping and visual preview jobs (Concurrency: ${activeConcurrency}).\n`);
112
+ });
113
+
114
+ socket.on("worker:registered", (data) => {
115
+ if (data?.concurrencyLimit && typeof data.concurrencyLimit === "number") {
116
+ activeConcurrency = data.concurrencyLimit;
117
+ logInfo(`[Cluster Config] Worker registered as '${data.name || data.workerId}'. Active concurrency: ${activeConcurrency} concurrent tabs.`);
118
+ }
119
+ });
120
+
121
+ socket.on("connect_error", (err) => {
122
+ if (err.message && err.message.includes("Authentication failed")) {
123
+ logError(`Authentication Failed: ${err.message}`);
124
+ logWarn("Please verify your worker key or run with '--reset' to enter a new key.");
125
+ } else {
126
+ logWarn(`Connection failed (${err.message}). Retrying in a few seconds...`);
127
+ }
128
+ });
129
+
130
+ socket.on("disconnect", (reason) => {
131
+ logWarn(`Disconnected from cluster: ${reason}`);
132
+ });
133
+
134
+ // Handle incoming job dispatch from server
135
+ socket.on("job:dispatch", async (job) => {
136
+ const jobId = job.jobId;
137
+ const jobType = job.jobType || job.type;
138
+ const payload = job.payload || {};
139
+ const targetUrl = jobType === "SEND_EMAIL"
140
+ ? (payload.mailOptions?.to ? `to: ${payload.mailOptions.to}` : "email")
141
+ : (payload.url || "(no url)");
142
+
143
+ logJob(jobType, targetUrl);
144
+ activeJobs++;
145
+
146
+ const startTime = Date.now();
147
+
148
+ try {
149
+ let resultData = null;
150
+
151
+ if (jobType === "SCRAPE_CHROMIUM") {
152
+ resultData = await handleChromiumScrape(payload);
153
+ } else if (jobType === "SCRAPE_HTTP") {
154
+ resultData = await handleHttpScrape(payload);
155
+ } else if (jobType === "PREVIEW_PAGE") {
156
+ resultData = await handlePreviewPage(payload);
157
+ } else if (jobType === "INSPECT_ELEMENT") {
158
+ resultData = await handleInspectElement(payload);
159
+ } else if (jobType === "SEND_EMAIL") {
160
+ resultData = await handleSendEmail(payload);
161
+ } else if (jobType === "AI_SMART_REPAIR") {
162
+ resultData = await handleAiSmartRepair(payload, config);
163
+ } else {
164
+ throw new Error(`Unsupported job type: ${jobType}`);
165
+ }
166
+
167
+ const durationMs = Date.now() - startTime;
168
+ logSuccess(`Completed ${jobType} for ${targetUrl} in ${durationMs}ms`);
169
+
170
+ sendJobResult({
171
+ jobId,
172
+ success: true,
173
+ data: resultData,
174
+ durationMs,
175
+ });
176
+ } catch (err) {
177
+ const durationMs = Date.now() - startTime;
178
+ logError(`Failed ${jobType} for ${targetUrl}: ${err.message}`);
179
+
180
+ sendJobResult({
181
+ jobId,
182
+ success: false,
183
+ error: {
184
+ message: err.message,
185
+ code: err.code || "JOB_EXECUTION_ERROR",
186
+ },
187
+ durationMs,
188
+ });
189
+ } finally {
190
+ activeJobs = Math.max(0, activeJobs - 1);
191
+ }
192
+ });
193
+
194
+ // Handle cluster heartbeat ping
195
+ socket.on("ping", () => {
196
+ socket.emit("pong", {
197
+ activeJobs,
198
+ memory: process.memoryUsage(),
199
+ uptime: process.uptime(),
200
+ });
201
+ });
202
+
203
+ // Render Anti-Sleep Keep-Alive Ping
204
+ let keepAliveTimer = null;
205
+ let initialKeepAliveTimer = null;
206
+
207
+ const clearKeepAliveTimers = () => {
208
+ if (keepAliveTimer) {
209
+ clearInterval(keepAliveTimer);
210
+ keepAliveTimer = null;
211
+ }
212
+ if (initialKeepAliveTimer) {
213
+ clearTimeout(initialKeepAliveTimer);
214
+ initialKeepAliveTimer = null;
215
+ }
216
+ };
217
+
218
+ const pingKeepAlive = async () => {
219
+ try {
220
+ // Try the dedicated keep-alive endpoint first
221
+ const endpoint = `${serverUrl}/api/workers/keep-alive`;
222
+ const res = await axios.get(endpoint, {
223
+ timeout: 15000,
224
+ headers: {
225
+ "x-worker-key": workerKey || "",
226
+ "user-agent": "AppsManagerEdgeWorker/1.0.4",
227
+ },
228
+ });
229
+ logInfo(`[Keep-Alive] Server awake ping OK (${res.status}) - Render instance active`);
230
+ } catch (err) {
231
+ // Fallback to /health
232
+ try {
233
+ const fallbackUrl = `${serverUrl}/health`;
234
+ await axios.get(fallbackUrl, {
235
+ timeout: 10000,
236
+ headers: {
237
+ "user-agent": "AppsManagerEdgeWorker/1.0.4",
238
+ },
239
+ });
240
+ logInfo(`[Keep-Alive] Fallback health ping OK - Render instance active`);
241
+ } catch (fallbackErr) {
242
+ logWarn(`[Keep-Alive] Server awake ping warning (${err.message}) - will retry next cycle`);
243
+ }
244
+ }
245
+ };
246
+
247
+ const startKeepAlive = () => {
248
+ if (keepAliveMinutes <= 0) return;
249
+ clearKeepAliveTimers();
250
+
251
+ // Send initial ping 15s after startup to warm up/verify
252
+ initialKeepAliveTimer = setTimeout(pingKeepAlive, 15000);
253
+ if (initialKeepAliveTimer.unref) initialKeepAliveTimer.unref();
254
+
255
+ const intervalMs = Math.max(1, keepAliveMinutes) * 60 * 1000;
256
+ keepAliveTimer = setInterval(pingKeepAlive, intervalMs);
257
+ if (keepAliveTimer.unref) keepAliveTimer.unref();
258
+ };
259
+
260
+ if (keepAliveMinutes > 0) {
261
+ startKeepAlive();
262
+ }
263
+
264
+ // Clean up keep-alive timers on socket disconnect
265
+ socket.on("disconnect", (reason) => {
266
+ logWarn(`Disconnected from cluster: ${reason}`);
267
+ clearKeepAliveTimers();
268
+ });
269
+
270
+ socket.on("connect", () => {
271
+ if (keepAliveMinutes > 0 && !keepAliveTimer) {
272
+ startKeepAlive();
273
+ }
274
+ });
275
+
276
+ // Provide programmatic close for automated test harnesses
277
+ socket.closeClient = async () => {
278
+ clearKeepAliveTimers();
279
+ if (isTermux) {
280
+ try {
281
+ execSync("termux-wake-unlock", { stdio: "ignore" });
282
+ } catch {}
283
+ }
284
+ socket.disconnect();
285
+ await localBrowserManager.close();
286
+ };
287
+
288
+ // Graceful shutdown handling
289
+ const shutdown = async () => {
290
+ logInfo("\nGracefully shutting down edge worker...");
291
+ await socket.closeClient();
292
+ logSuccess("Edge worker stopped cleanly.");
293
+ process.exit(0);
294
+ };
295
+
296
+ process.on("SIGINT", shutdown);
297
+ process.on("SIGTERM", shutdown);
298
+
299
+ return socket;
300
+ }
package/src/config.js CHANGED
@@ -111,7 +111,7 @@ export async function resolveWorkerConfig() {
111
111
  const cli = parseCliArgs();
112
112
 
113
113
  if (cli.help) {
114
- let pkgVer = "1.0.8";
114
+ let pkgVer = "1.0.9";
115
115
  try {
116
116
  const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
117
117
  if (pkg?.version) pkgVer = pkg.version;