@airtop/plugin-batch-operate 1.0.0-alpha2.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Airtop
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.cjs ADDED
@@ -0,0 +1,509 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } var _class; var _class2;// src/batch-operate.plugin.ts
2
+ var _sdk = require('@airtop/sdk');
3
+
4
+ // src/lib/batch-util.ts
5
+ var _eventemitter3 = require('eventemitter3');
6
+
7
+ // src/lib/SessionQueue.ts
8
+ var _asyncmutex = require('async-mutex');
9
+
10
+ // src/lib/WindowQueue.ts
11
+
12
+ var WindowQueue = (_class = class {
13
+ __init() {this.activePromises = []}
14
+ __init2() {this.urlQueue = []}
15
+ __init3() {this.activePromisesMutex = new (0, _asyncmutex.Mutex)()}
16
+ __init4() {this.urlQueueMutex = new (0, _asyncmutex.Mutex)()}
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+ constructor(maxWindowsPerSession, runEmitter, sessionId, client, operation, onError, isHalted = false) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this);_class.prototype.__init4.call(this);
26
+ if (!Number.isInteger(maxWindowsPerSession) || maxWindowsPerSession <= 0) {
27
+ throw new Error("maxWindowsPerSession must be a positive integer");
28
+ }
29
+ this.maxWindowsPerSession = maxWindowsPerSession;
30
+ this.runEmitter = runEmitter;
31
+ this.sessionId = sessionId;
32
+ this.client = client;
33
+ this.operation = operation;
34
+ this.onError = onError;
35
+ this.isHalted = isHalted;
36
+ this.logger = this.client.getLogger();
37
+ }
38
+ async addUrlToQueue(url) {
39
+ await this.urlQueueMutex.runExclusive(() => {
40
+ this.urlQueue.push(url);
41
+ });
42
+ }
43
+ handleHaltEvent() {
44
+ this.logger.info("Halt event received");
45
+ this.isHalted = true;
46
+ }
47
+ async processInBatches(urls) {
48
+ const results = [];
49
+ this.runEmitter.on("halt", this.handleHaltEvent.bind(this));
50
+ await this.urlQueueMutex.runExclusive(() => {
51
+ this.urlQueue = [...urls];
52
+ });
53
+ this.logger.withMetadata({
54
+ urls,
55
+ sessionId: this.sessionId
56
+ }).info(`Processing batch for session ${this.sessionId}`);
57
+ while (this.urlQueue.length > 0) {
58
+ let shouldContinue = false;
59
+ await this.activePromisesMutex.runExclusive(async () => {
60
+ if (this.activePromises.length >= this.maxWindowsPerSession) {
61
+ await Promise.race(this.activePromises);
62
+ shouldContinue = true;
63
+ }
64
+ });
65
+ if (shouldContinue) continue;
66
+ let urlData;
67
+ await this.urlQueueMutex.runExclusive(() => {
68
+ urlData = this.urlQueue.shift();
69
+ });
70
+ if (!urlData) break;
71
+ const promise = (async () => {
72
+ if (this.isHalted) {
73
+ this.logger.info(`Processing halted, skipping window creation for ${urlData.url}`);
74
+ return;
75
+ }
76
+ let windowId;
77
+ let liveViewUrl;
78
+ try {
79
+ this.logger.info(`Creating window for ${urlData.url} in session ${this.sessionId}`);
80
+ const { data, errors, warnings } = await this.client.withSessionId(this.sessionId).createWindow(urlData.url);
81
+ windowId = data.windowId;
82
+ this.handleErrorAndWarningResponses({
83
+ warnings,
84
+ errors,
85
+ sessionId: this.sessionId,
86
+ url: urlData,
87
+ operation: "window creation"
88
+ });
89
+ if (!windowId) {
90
+ throw new Error(`WindowId not found, errors: ${JSON.stringify(errors)}`);
91
+ }
92
+ const {
93
+ data: windowInfo,
94
+ warnings: windowWarnings,
95
+ errors: windowErrors
96
+ } = await this.client.withSessionId(this.sessionId).withWindowId(windowId).getWindowInfo();
97
+ liveViewUrl = windowInfo.liveViewUrl;
98
+ this.handleErrorAndWarningResponses({
99
+ warnings: windowWarnings,
100
+ errors: windowErrors,
101
+ sessionId: this.sessionId,
102
+ url: urlData,
103
+ operation: "window info retrieval"
104
+ });
105
+ const result = await this.operation({
106
+ windowId,
107
+ sessionId: this.sessionId,
108
+ liveViewUrl,
109
+ operationUrl: urlData
110
+ });
111
+ if (result) {
112
+ const { shouldHaltBatch, additionalUrls, data: data2 } = result;
113
+ if (data2) {
114
+ results.push(data2);
115
+ }
116
+ if (shouldHaltBatch) {
117
+ this.logger.info("Emitting halt event");
118
+ this.runEmitter.emit("halt");
119
+ }
120
+ if (additionalUrls && additionalUrls.length > 0) {
121
+ this.logger.withMetadata({
122
+ additionalUrls
123
+ }).info("Emitting addUrls event");
124
+ this.runEmitter.emit("addUrls", additionalUrls);
125
+ }
126
+ }
127
+ } catch (error) {
128
+ if (this.onError) {
129
+ await this.handleErrorWithCallback({
130
+ originalError: error,
131
+ url: urlData,
132
+ callback: this.onError,
133
+ windowId,
134
+ liveViewUrl
135
+ });
136
+ } else {
137
+ const message = `Error for URL ${urlData.url}: ${this.formatError(error)}`;
138
+ this.logger.error(message);
139
+ }
140
+ } finally {
141
+ if (windowId) {
142
+ await this.safelyTerminateWindow(windowId);
143
+ }
144
+ }
145
+ })();
146
+ await this.activePromisesMutex.runExclusive(() => {
147
+ this.activePromises.push(promise);
148
+ });
149
+ promise.finally(async () => {
150
+ await this.activePromisesMutex.runExclusive(() => {
151
+ const index = this.activePromises.indexOf(promise);
152
+ if (index > -1) {
153
+ this.activePromises.splice(index, 1);
154
+ }
155
+ });
156
+ });
157
+ }
158
+ await Promise.allSettled(this.activePromises);
159
+ this.runEmitter.removeListener("halt", this.handleHaltEvent);
160
+ return results;
161
+ }
162
+ async handleErrorWithCallback({
163
+ originalError,
164
+ url,
165
+ windowId,
166
+ liveViewUrl,
167
+ callback
168
+ }) {
169
+ try {
170
+ await callback({
171
+ error: this.formatError(originalError),
172
+ operationUrls: [url],
173
+ sessionId: this.sessionId,
174
+ windowId,
175
+ liveViewUrl
176
+ });
177
+ } catch (newError) {
178
+ this.logger.error(
179
+ `Error in onError callback: ${this.formatError(newError)}. Original error: ${this.formatError(originalError)}`
180
+ );
181
+ }
182
+ }
183
+ async safelyTerminateWindow(windowId) {
184
+ try {
185
+ await this.client.withSessionId(this.sessionId).withWindowId(windowId).close();
186
+ } catch (error) {
187
+ this.logger.withError(error).error(`Error closing window ${windowId}`);
188
+ }
189
+ }
190
+ formatError(error) {
191
+ return error instanceof Error ? error.message : String(error);
192
+ }
193
+ handleErrorAndWarningResponses({
194
+ warnings,
195
+ errors,
196
+ sessionId,
197
+ url,
198
+ operation
199
+ }) {
200
+ if (!warnings && !errors) return;
201
+ const details = {
202
+ sessionId,
203
+ url
204
+ };
205
+ if (warnings) {
206
+ details.warnings = warnings;
207
+ this.logger.withMetadata({
208
+ details
209
+ }).warn(`Received warnings for ${operation}`);
210
+ }
211
+ if (errors) {
212
+ details.errors = errors;
213
+ this.logger.withMetadata({
214
+ details
215
+ }).error(`Received errors for ${operation}`);
216
+ }
217
+ }
218
+ }, _class);
219
+
220
+ // src/lib/helpers.ts
221
+ var distributeUrlsToBatches = (urls, maxConcurrentSessions) => {
222
+ if (urls.length === 0) return [];
223
+ const batchCount = Math.min(maxConcurrentSessions, urls.length);
224
+ const batches = Array.from({ length: batchCount }, () => []);
225
+ urls.forEach((url, index) => {
226
+ const batchIndex = index % batchCount;
227
+ if (!batches[batchIndex]) {
228
+ batches[batchIndex] = [];
229
+ }
230
+ batches[batchIndex].push(url);
231
+ });
232
+ return batches;
233
+ };
234
+
235
+ // src/lib/SessionQueue.ts
236
+ var SessionQueue = (_class2 = class {
237
+ __init5() {this.activePromises = []}
238
+ __init6() {this.activePromisesMutex = new (0, _asyncmutex.Mutex)()}
239
+
240
+
241
+
242
+
243
+ __init7() {this.initialBatches = []}
244
+
245
+
246
+
247
+ __init8() {this.batchQueue = []}
248
+ __init9() {this.batchQueueMutex = new (0, _asyncmutex.Mutex)()}
249
+ __init10() {this.latestProcessingPromise = null}
250
+ __init11() {this.processingPromisesCount = 0}
251
+
252
+ __init12() {this.sessionPool = []}
253
+ __init13() {this.sessionPoolMutex = new (0, _asyncmutex.Mutex)()}
254
+
255
+
256
+ constructor({
257
+ maxConcurrentSessions,
258
+ runEmitter,
259
+ maxWindowsPerSession,
260
+ initialBatches,
261
+ operation,
262
+ client,
263
+ sessionConfig,
264
+ onError
265
+ }) {;_class2.prototype.__init5.call(this);_class2.prototype.__init6.call(this);_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);_class2.prototype.__init11.call(this);_class2.prototype.__init12.call(this);_class2.prototype.__init13.call(this);
266
+ if (!Number.isInteger(maxConcurrentSessions) || maxConcurrentSessions <= 0) {
267
+ throw new Error("maxConcurrentSessions must be a positive integer");
268
+ }
269
+ this.maxConcurrentSessions = maxConcurrentSessions;
270
+ this.runEmitter = runEmitter;
271
+ this.maxWindowsPerSession = maxWindowsPerSession;
272
+ this.sessionConfig = sessionConfig;
273
+ this.initialBatches = initialBatches;
274
+ this.operation = operation;
275
+ this.onError = onError;
276
+ this.latestProcessingPromise = null;
277
+ this.results = [];
278
+ this.client = client;
279
+ this.isHalted = false;
280
+ this.logger = client.getLogger();
281
+ }
282
+ handleHaltEvent() {
283
+ this.logger.info("Halt event received");
284
+ this.isHalted = true;
285
+ }
286
+ async addUrlsToBatchQueue(newBatch) {
287
+ const newBatches = distributeUrlsToBatches(newBatch, this.maxConcurrentSessions);
288
+ this.logger.withMetadata({
289
+ newBatches
290
+ }).info("Adding new batches to queue");
291
+ await this.batchQueueMutex.runExclusive(() => {
292
+ this.batchQueue.push(...newBatches);
293
+ });
294
+ this.processingPromisesCount++;
295
+ this.latestProcessingPromise = this.processPendingBatches();
296
+ }
297
+ async processInitialBatches() {
298
+ await this.batchQueueMutex.runExclusive(() => {
299
+ this.batchQueue = [...this.initialBatches];
300
+ });
301
+ this.processingPromisesCount++;
302
+ this.runEmitter.on("halt", this.handleHaltEvent.bind(this));
303
+ this.latestProcessingPromise = this.processPendingBatches();
304
+ await this.latestProcessingPromise;
305
+ }
306
+ async waitForProcessingToComplete() {
307
+ while (this.processingPromisesCount > 0) {
308
+ await this.latestProcessingPromise;
309
+ }
310
+ await this.terminateAllSessions();
311
+ this.runEmitter.removeListener("halt", this.handleHaltEvent);
312
+ return this.results;
313
+ }
314
+ async terminateAllSessions() {
315
+ for (const sessionId of this.sessionPool) {
316
+ this.safelyTerminateSession(sessionId);
317
+ }
318
+ }
319
+ async processPendingBatches() {
320
+ try {
321
+ while (this.batchQueue.length > 0) {
322
+ let shouldContinue = false;
323
+ await this.activePromisesMutex.runExclusive(async () => {
324
+ if (this.activePromises.length >= this.maxConcurrentSessions) {
325
+ await Promise.race(this.activePromises);
326
+ shouldContinue = true;
327
+ }
328
+ });
329
+ if (shouldContinue) continue;
330
+ let batch;
331
+ await this.batchQueueMutex.runExclusive(() => {
332
+ batch = this.batchQueue.shift();
333
+ });
334
+ if (!batch || batch.length === 0) break;
335
+ const promise = (async () => {
336
+ if (this.isHalted) {
337
+ this.logger.info("Halt event received, skipping batch");
338
+ return;
339
+ }
340
+ let sessionId;
341
+ try {
342
+ await this.sessionPoolMutex.runExclusive(() => {
343
+ if (this.sessionPool.length > 0) {
344
+ sessionId = this.sessionPool.pop();
345
+ }
346
+ });
347
+ if (!sessionId) {
348
+ const { data: session, warnings, errors } = await this.client.createSession(this.sessionConfig);
349
+ sessionId = session.id;
350
+ this.handleErrorAndWarningResponses({ warnings, errors, sessionId, batch });
351
+ }
352
+ const queue = new WindowQueue(
353
+ this.maxWindowsPerSession,
354
+ this.runEmitter,
355
+ sessionId,
356
+ this.client,
357
+ this.operation,
358
+ this.onError,
359
+ this.isHalted
360
+ );
361
+ const windowResults = await queue.processInBatches(batch);
362
+ this.results.push(...windowResults);
363
+ await this.sessionPoolMutex.runExclusive(() => {
364
+ if (!sessionId) {
365
+ throw new Error("Missing sessionId, cannot return to pool");
366
+ }
367
+ this.sessionPool.push(sessionId);
368
+ });
369
+ } catch (error) {
370
+ if (this.onError) {
371
+ await this.handleErrorWithCallback({ originalError: error, batch, sessionId, callback: this.onError });
372
+ } else {
373
+ const urls = batch.map((url) => url.url);
374
+ this.logErrorForUrls(urls, error);
375
+ }
376
+ if (sessionId) {
377
+ this.safelyTerminateSession(sessionId);
378
+ }
379
+ }
380
+ })();
381
+ await this.activePromisesMutex.runExclusive(() => {
382
+ this.activePromises.push(promise);
383
+ });
384
+ promise.finally(async () => {
385
+ await this.activePromisesMutex.runExclusive(() => {
386
+ const index = this.activePromises.indexOf(promise);
387
+ if (index > -1) {
388
+ this.activePromises.splice(index, 1);
389
+ }
390
+ });
391
+ });
392
+ }
393
+ await Promise.allSettled(this.activePromises);
394
+ } finally {
395
+ this.processingPromisesCount--;
396
+ }
397
+ }
398
+ async handleErrorWithCallback({
399
+ originalError,
400
+ batch,
401
+ sessionId,
402
+ callback
403
+ }) {
404
+ try {
405
+ await callback({
406
+ error: this.formatError(originalError),
407
+ operationUrls: batch,
408
+ sessionId
409
+ });
410
+ } catch (newError) {
411
+ this.logger.error(
412
+ `Error in onError callback: ${this.formatError(newError)}. Original error: ${this.formatError(originalError)}`
413
+ );
414
+ }
415
+ }
416
+ logErrorForUrls(urls, error) {
417
+ this.logger.withError(error).withMetadata({
418
+ urls
419
+ }).error("Error for URLs");
420
+ }
421
+ safelyTerminateSession(sessionId) {
422
+ this.client.withSessionId(sessionId).terminateSession().catch((error) => {
423
+ this.logger.withMetadata({
424
+ sessionId
425
+ }).withError(error).error(`Error terminating session ${sessionId}`);
426
+ });
427
+ }
428
+ formatError(error) {
429
+ return error instanceof Error ? error.message : String(error);
430
+ }
431
+ handleErrorAndWarningResponses({
432
+ warnings,
433
+ errors,
434
+ sessionId,
435
+ batch
436
+ }) {
437
+ if (!warnings && !errors) return;
438
+ const details = {
439
+ sessionId,
440
+ urls: batch
441
+ };
442
+ if (warnings) {
443
+ details.warnings = warnings;
444
+ this.logger.withMetadata({
445
+ details
446
+ }).warn("Received warnings creating session");
447
+ }
448
+ if (errors) {
449
+ details.errors = errors;
450
+ this.logger.withMetadata({ details }).error("Received errors creating session");
451
+ }
452
+ }
453
+ }, _class2);
454
+
455
+ // src/lib/batch-util.ts
456
+ var DEFAULT_MAX_WINDOWS_PER_SESSION = 1;
457
+ var DEFAULT_MAX_CONCURRENT_SESSIONS = 30;
458
+ var batchOperate = async (urls, operation, client, config) => {
459
+ if (!Array.isArray(urls)) {
460
+ throw new Error("Please provide a valid list of urls");
461
+ }
462
+ for (const url of urls) {
463
+ if (!url || typeof url !== "object" || !("url" in url)) {
464
+ throw new Error("Please provide a valid list of urls");
465
+ }
466
+ }
467
+ const runEmitter = new (0, _eventemitter3.EventEmitter)();
468
+ const {
469
+ maxConcurrentSessions = DEFAULT_MAX_CONCURRENT_SESSIONS,
470
+ maxWindowsPerSession = DEFAULT_MAX_WINDOWS_PER_SESSION,
471
+ sessionConfig,
472
+ onError
473
+ } = _nullishCoalesce(config, () => ( {}));
474
+ const initialBatches = distributeUrlsToBatches(urls, maxConcurrentSessions);
475
+ const sessionQueue = new SessionQueue({
476
+ maxConcurrentSessions,
477
+ runEmitter,
478
+ maxWindowsPerSession,
479
+ initialBatches,
480
+ operation,
481
+ client,
482
+ sessionConfig,
483
+ onError
484
+ });
485
+ runEmitter.on("addUrls", (additionalUrls) => {
486
+ sessionQueue.addUrlsToBatchQueue(additionalUrls);
487
+ });
488
+ await sessionQueue.processInitialBatches();
489
+ return await sessionQueue.waitForProcessingToComplete();
490
+ };
491
+
492
+ // src/batch-operate.plugin.ts
493
+ var plugin = {
494
+ augmentationType: _sdk.AirtopPluginAugmentationType.AirtopClient,
495
+ augment: (prototype) => {
496
+ prototype.batchOperate = async function(urls, operation, config) {
497
+ return await batchOperate(urls, operation, this, config);
498
+ };
499
+ }
500
+ };
501
+ function batchOperatePlugin() {
502
+ return {
503
+ pluginsToAdd: [plugin]
504
+ };
505
+ }
506
+
507
+
508
+ exports.batchOperatePlugin = batchOperatePlugin;
509
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/theo/projects/monarch/sdk-v2/wrapper-sdks/typescript/packages/plugins/batch-operate/dist/index.cjs","../src/batch-operate.plugin.ts","../src/lib/batch-util.ts","../src/lib/SessionQueue.ts","../src/lib/WindowQueue.ts","../src/lib/helpers.ts"],"names":["data"],"mappings":"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","file":"/Users/theo/projects/monarch/sdk-v2/wrapper-sdks/typescript/packages/plugins/batch-operate/dist/index.cjs","sourcesContent":[null,"import { AirtopPluginAugmentationType } from \"@airtop/sdk\";\nimport type { AirtopPluginRegistration } from \"@airtop/sdk\";\nimport type { AirtopClientPlugin } from \"@airtop/sdk\";\nimport type { AirtopClient } from \"@airtop/sdk\";\nimport { batchOperate } from \"./lib/batch-util.js\";\nimport type {\n BatchOperateConfig,\n BatchOperationInput,\n BatchOperationResponse,\n BatchOperationUrl,\n} from \"./lib/types.js\";\n\n// This is exported so the docs can expose it as we can't generate docs against the\n// module declaration\n/**\n * The methods provided by the Batch Operate plugin.\n */\nexport interface BatchOperateMethods {\n batchOperate: <T>(\n urls: BatchOperationUrl[],\n operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>,\n config?: BatchOperateConfig,\n ) => Promise<T[]>;\n}\n\ndeclare module \"@airtop/sdk\" {\n interface AirtopClient {\n batchOperate<T>(\n urls: BatchOperationUrl[],\n operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>,\n config?: BatchOperateConfig,\n ): Promise<T[]>;\n }\n}\n\nconst plugin: AirtopClientPlugin = {\n augmentationType: AirtopPluginAugmentationType.AirtopClient,\n augment: (prototype: typeof AirtopClient.prototype) => {\n prototype.batchOperate = async function <T>(\n urls: BatchOperationUrl[],\n operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>,\n config?: BatchOperateConfig,\n ): Promise<T[]> {\n return await batchOperate(urls, operation, this, config);\n };\n },\n};\n\n/**\n * Use with registerAirtopPlugin() from the Airtop SDK to add this plugin.\n */\nexport function batchOperatePlugin(): AirtopPluginRegistration {\n return {\n pluginsToAdd: [plugin],\n };\n}\n","import type { AirtopClient } from \"@airtop/sdk\";\nimport { EventEmitter } from \"eventemitter3\";\nimport { SessionQueue } from \"./SessionQueue.js\";\nimport { distributeUrlsToBatches } from \"./helpers.js\";\nimport type { BatchOperateConfig, BatchOperationInput, BatchOperationResponse, BatchOperationUrl } from \"./types.js\";\n\nconst DEFAULT_MAX_WINDOWS_PER_SESSION = 1;\nconst DEFAULT_MAX_CONCURRENT_SESSIONS = 30;\n\nexport const batchOperate = async <T>(\n urls: BatchOperationUrl[],\n operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>, // operation to invoke on each url\n client: AirtopClient,\n config?: BatchOperateConfig,\n): Promise<T[]> => {\n // Validate the urls before proceeding\n if (!Array.isArray(urls)) {\n throw new Error(\"Please provide a valid list of urls\");\n }\n\n for (const url of urls) {\n if (!url || typeof url !== \"object\" || !(\"url\" in url)) {\n throw new Error(\"Please provide a valid list of urls\");\n }\n }\n\n const runEmitter = new EventEmitter();\n\n const {\n maxConcurrentSessions = DEFAULT_MAX_CONCURRENT_SESSIONS,\n maxWindowsPerSession = DEFAULT_MAX_WINDOWS_PER_SESSION,\n sessionConfig,\n onError,\n } = config ?? {};\n\n // Split the urls into batches\n const initialBatches = distributeUrlsToBatches(urls, maxConcurrentSessions);\n const sessionQueue = new SessionQueue({\n maxConcurrentSessions,\n runEmitter,\n maxWindowsPerSession,\n initialBatches,\n operation,\n client,\n sessionConfig,\n onError,\n });\n\n runEmitter.on(\"addUrls\", (additionalUrls: BatchOperationUrl[]) => {\n sessionQueue.addUrlsToBatchQueue(additionalUrls);\n });\n\n await sessionQueue.processInitialBatches();\n\n return await sessionQueue.waitForProcessingToComplete();\n};\n","import type { Issue } from \"@airtop/sdk\";\nimport type { AirtopClient, ILogLayer } from \"@airtop/sdk\";\nimport type { CreateSessionConfig } from \"@airtop/sdk\";\nimport { Mutex } from \"async-mutex\";\nimport type { EventEmitter } from \"eventemitter3\";\nimport { WindowQueue } from \"./WindowQueue.js\";\nimport { distributeUrlsToBatches } from \"./helpers.js\";\nimport type { BatchOperationError, BatchOperationInput, BatchOperationResponse, BatchOperationUrl } from \"./types.js\";\n\nexport class SessionQueue<T> {\n private activePromises: Promise<void>[] = [];\n private activePromisesMutex = new Mutex();\n\n private maxConcurrentSessions: number;\n private runEmitter: EventEmitter;\n private maxWindowsPerSession: number;\n private sessionConfig?: CreateSessionConfig;\n private initialBatches: BatchOperationUrl[][] = [];\n private operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>;\n private onError?: (error: BatchOperationError) => Promise<void>;\n private isHalted: boolean;\n\n private batchQueue: BatchOperationUrl[][] = [];\n private batchQueueMutex = new Mutex();\n private latestProcessingPromise: Promise<void> | null = null;\n private processingPromisesCount = 0;\n\n private client: AirtopClient;\n private sessionPool: string[] = [];\n private sessionPoolMutex = new Mutex();\n\n private results: T[];\n private logger: ILogLayer;\n\n constructor({\n maxConcurrentSessions,\n runEmitter,\n maxWindowsPerSession,\n initialBatches,\n operation,\n client,\n sessionConfig,\n onError,\n }: {\n maxConcurrentSessions: number;\n runEmitter: EventEmitter;\n maxWindowsPerSession: number;\n initialBatches: BatchOperationUrl[][];\n operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>;\n client: AirtopClient;\n sessionConfig?: CreateSessionConfig;\n onError?: (error: BatchOperationError) => Promise<void>;\n }) {\n if (!Number.isInteger(maxConcurrentSessions) || maxConcurrentSessions <= 0) {\n throw new Error(\"maxConcurrentSessions must be a positive integer\");\n }\n\n this.maxConcurrentSessions = maxConcurrentSessions;\n this.runEmitter = runEmitter;\n this.maxWindowsPerSession = maxWindowsPerSession;\n this.sessionConfig = sessionConfig;\n this.initialBatches = initialBatches;\n this.operation = operation;\n this.onError = onError;\n this.latestProcessingPromise = null;\n this.results = [];\n this.client = client;\n this.isHalted = false;\n this.logger = client.getLogger();\n }\n\n public handleHaltEvent(): void {\n this.logger.info(\"Halt event received\");\n this.isHalted = true;\n }\n\n public async addUrlsToBatchQueue(newBatch: BatchOperationUrl[]): Promise<void> {\n // Distribute new URLs across batches\n const newBatches = distributeUrlsToBatches(newBatch, this.maxConcurrentSessions);\n\n this.logger\n .withMetadata({\n newBatches,\n })\n .info(\"Adding new batches to queue\");\n\n // Add new batches to the queue\n await this.batchQueueMutex.runExclusive(() => {\n this.batchQueue.push(...newBatches);\n });\n\n // Update existing processing promise\n this.processingPromisesCount++;\n this.latestProcessingPromise = this.processPendingBatches();\n }\n\n public async processInitialBatches(): Promise<void> {\n await this.batchQueueMutex.runExclusive(() => {\n this.batchQueue = [...this.initialBatches];\n });\n this.processingPromisesCount++;\n this.runEmitter.on(\"halt\", this.handleHaltEvent.bind(this));\n this.latestProcessingPromise = this.processPendingBatches();\n await this.latestProcessingPromise;\n }\n\n public async waitForProcessingToComplete(): Promise<T[]> {\n while (this.processingPromisesCount > 0) {\n await this.latestProcessingPromise;\n }\n\n await this.terminateAllSessions();\n\n this.runEmitter.removeListener(\"halt\", this.handleHaltEvent);\n\n return this.results;\n }\n\n private async terminateAllSessions(): Promise<void> {\n for (const sessionId of this.sessionPool) {\n this.safelyTerminateSession(sessionId);\n }\n }\n\n private async processPendingBatches(): Promise<void> {\n try {\n while (this.batchQueue.length > 0) {\n // Wait for any session to complete before starting a new one\n let shouldContinue = false;\n await this.activePromisesMutex.runExclusive(async () => {\n if (this.activePromises.length >= this.maxConcurrentSessions) {\n await Promise.race(this.activePromises);\n shouldContinue = true;\n }\n });\n\n if (shouldContinue) continue;\n\n let batch: BatchOperationUrl[] | undefined;\n await this.batchQueueMutex.runExclusive(() => {\n batch = this.batchQueue.shift();\n });\n\n if (!batch || batch.length === 0) break;\n\n const promise = (async () => {\n if (this.isHalted) {\n this.logger.info(\"Halt event received, skipping batch\");\n return;\n }\n\n let sessionId: string | undefined;\n try {\n // Check if there's an available session in the pool\n await this.sessionPoolMutex.runExclusive(() => {\n if (this.sessionPool.length > 0) {\n sessionId = this.sessionPool.pop();\n }\n });\n\n // Otherwise, create a new session\n if (!sessionId) {\n const { data: session, warnings, errors } = await this.client.createSession(this.sessionConfig);\n sessionId = session.id;\n\n this.handleErrorAndWarningResponses({ warnings, errors, sessionId, batch });\n }\n\n const queue = new WindowQueue(\n this.maxWindowsPerSession,\n this.runEmitter,\n sessionId,\n this.client,\n this.operation,\n this.onError,\n this.isHalted,\n );\n const windowResults = await queue.processInBatches(batch);\n this.results.push(...windowResults);\n\n // Return the session to the pool\n await this.sessionPoolMutex.runExclusive(() => {\n if (!sessionId) {\n throw new Error(\"Missing sessionId, cannot return to pool\");\n }\n\n this.sessionPool.push(sessionId);\n });\n } catch (error) {\n if (this.onError) {\n await this.handleErrorWithCallback({ originalError: error, batch, sessionId, callback: this.onError });\n } else {\n // By default, log the error and continue\n const urls = batch.map((url) => url.url);\n this.logErrorForUrls(urls, error);\n }\n\n // Clean up the session in case of error\n if (sessionId) {\n this.safelyTerminateSession(sessionId);\n }\n }\n })();\n\n await this.activePromisesMutex.runExclusive(() => {\n this.activePromises.push(promise);\n });\n\n // Remove the promise when it completes\n promise.finally(async () => {\n await this.activePromisesMutex.runExclusive(() => {\n const index = this.activePromises.indexOf(promise);\n if (index > -1) {\n this.activePromises.splice(index, 1);\n }\n });\n });\n }\n\n // Wait for all remaining sessions to complete\n await Promise.allSettled(this.activePromises);\n } finally {\n this.processingPromisesCount--;\n }\n }\n\n private async handleErrorWithCallback({\n originalError,\n batch,\n sessionId,\n callback,\n }: {\n originalError: unknown;\n batch: BatchOperationUrl[];\n sessionId?: string;\n callback: (error: BatchOperationError) => Promise<void>;\n }): Promise<void> {\n // Catch any errors in the onError callback to avoid halting the entire process\n try {\n await callback({\n error: this.formatError(originalError),\n operationUrls: batch,\n sessionId,\n });\n } catch (newError) {\n this.logger.error(\n `Error in onError callback: ${this.formatError(newError)}. Original error: ${this.formatError(originalError)}`,\n );\n }\n }\n\n private logErrorForUrls(urls: string[], error: unknown): void {\n this.logger\n .withError(error)\n .withMetadata({\n urls,\n })\n .error(\"Error for URLs\");\n }\n\n private safelyTerminateSession(sessionId: string): void {\n // Do not await since we don't want to block the main thread\n this.client\n .withSessionId(sessionId)\n .terminateSession()\n .catch((error) => {\n this.logger\n .withMetadata({\n sessionId,\n })\n .withError(error)\n .error(`Error terminating session ${sessionId}`);\n });\n }\n\n private formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n }\n\n private handleErrorAndWarningResponses({\n warnings,\n errors,\n sessionId,\n batch,\n }: { warnings?: Issue[]; errors?: Issue[]; sessionId: string; batch: BatchOperationUrl[] }): void {\n if (!warnings && !errors) return;\n\n const details: { sessionId: string; urls: BatchOperationUrl[]; warnings?: Issue[]; errors?: Issue[] } = {\n sessionId,\n urls: batch,\n };\n\n if (warnings) {\n details.warnings = warnings;\n this.logger\n .withMetadata({\n details,\n })\n .warn(\"Received warnings creating session\");\n }\n\n // Log an object with the errors and the URL\n if (errors) {\n details.errors = errors;\n this.logger.withMetadata({ details }).error(\"Received errors creating session\");\n }\n }\n}\n","import type { Issue } from \"@airtop/sdk\";\nimport type { AirtopClient } from \"@airtop/sdk\";\nimport type { ILogLayer } from \"@airtop/sdk\";\nimport { Mutex } from \"async-mutex\";\nimport type { EventEmitter } from \"eventemitter3\";\nimport type { BatchOperationError, BatchOperationInput, BatchOperationResponse, BatchOperationUrl } from \"./types.js\";\n\nexport class WindowQueue<T> {\n private activePromises: Promise<void>[] = [];\n private urlQueue: BatchOperationUrl[] = [];\n private activePromisesMutex = new Mutex();\n private urlQueueMutex = new Mutex();\n\n private maxWindowsPerSession: number;\n private runEmitter: EventEmitter;\n private sessionId: string;\n private client: AirtopClient;\n private operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>;\n private onError?: (error: BatchOperationError) => Promise<void>;\n private isHalted;\n private logger: ILogLayer;\n\n constructor(\n maxWindowsPerSession: number,\n runEmitter: EventEmitter,\n sessionId: string,\n client: AirtopClient,\n operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>,\n onError?: (error: BatchOperationError) => Promise<void>,\n isHalted = false,\n ) {\n if (!Number.isInteger(maxWindowsPerSession) || maxWindowsPerSession <= 0) {\n throw new Error(\"maxWindowsPerSession must be a positive integer\");\n }\n\n this.maxWindowsPerSession = maxWindowsPerSession;\n this.runEmitter = runEmitter;\n this.sessionId = sessionId;\n this.client = client;\n this.operation = operation;\n this.onError = onError;\n this.isHalted = isHalted;\n this.logger = this.client.getLogger();\n }\n\n public async addUrlToQueue(url: BatchOperationUrl): Promise<void> {\n await this.urlQueueMutex.runExclusive(() => {\n this.urlQueue.push(url);\n });\n }\n\n private handleHaltEvent(): void {\n this.logger.info(\"Halt event received\");\n this.isHalted = true;\n }\n\n async processInBatches(urls: BatchOperationUrl[]): Promise<T[]> {\n const results: T[] = [];\n this.runEmitter.on(\"halt\", this.handleHaltEvent.bind(this));\n\n await this.urlQueueMutex.runExclusive(() => {\n this.urlQueue = [...urls];\n });\n this.logger\n .withMetadata({\n urls,\n sessionId: this.sessionId,\n })\n .info(`Processing batch for session ${this.sessionId}`);\n\n while (this.urlQueue.length > 0) {\n // Wait for any window to complete before starting a new one\n let shouldContinue = false;\n await this.activePromisesMutex.runExclusive(async () => {\n if (this.activePromises.length >= this.maxWindowsPerSession) {\n await Promise.race(this.activePromises);\n shouldContinue = true;\n }\n });\n\n if (shouldContinue) continue;\n\n let urlData: BatchOperationUrl | undefined;\n await this.urlQueueMutex.runExclusive(() => {\n urlData = this.urlQueue.shift(); // Take the next url from the queue\n });\n if (!urlData) break; // No more urls to process\n\n // If we have less than the max concurrent operations, start a new one\n const promise = (async () => {\n // Do not process any more urls if the processing has been halted\n if (this.isHalted) {\n this.logger.info(`Processing halted, skipping window creation for ${urlData.url}`);\n return;\n }\n\n let windowId: string | undefined;\n let liveViewUrl: string | undefined;\n try {\n // Create a new window pointed to the url\n this.logger.info(`Creating window for ${urlData.url} in session ${this.sessionId}`);\n const { data, errors, warnings } = await this.client.withSessionId(this.sessionId).createWindow(urlData.url);\n\n windowId = data.windowId;\n\n this.handleErrorAndWarningResponses({\n warnings,\n errors,\n sessionId: this.sessionId,\n url: urlData,\n operation: \"window creation\",\n });\n\n if (!windowId) {\n throw new Error(`WindowId not found, errors: ${JSON.stringify(errors)}`);\n }\n\n const {\n data: windowInfo,\n warnings: windowWarnings,\n errors: windowErrors,\n } = await this.client.withSessionId(this.sessionId).withWindowId(windowId).getWindowInfo();\n liveViewUrl = windowInfo.liveViewUrl;\n\n this.handleErrorAndWarningResponses({\n warnings: windowWarnings,\n errors: windowErrors,\n sessionId: this.sessionId,\n url: urlData,\n operation: \"window info retrieval\",\n });\n\n // Run the operation on the window\n const result = await this.operation({\n windowId,\n sessionId: this.sessionId,\n liveViewUrl,\n operationUrl: urlData,\n });\n\n if (result) {\n const { shouldHaltBatch, additionalUrls, data } = result;\n\n if (data) {\n results.push(data);\n }\n\n if (shouldHaltBatch) {\n this.logger.info(\"Emitting halt event\");\n this.runEmitter.emit(\"halt\");\n }\n\n if (additionalUrls && additionalUrls.length > 0) {\n this.logger\n .withMetadata({\n additionalUrls,\n })\n .info(\"Emitting addUrls event\");\n this.runEmitter.emit(\"addUrls\", additionalUrls);\n }\n }\n } catch (error) {\n if (this.onError) {\n await this.handleErrorWithCallback({\n originalError: error,\n url: urlData,\n callback: this.onError,\n windowId,\n liveViewUrl,\n });\n } else {\n // By default, log the error and continue\n const message = `Error for URL ${urlData.url}: ${this.formatError(error)}`;\n this.logger.error(message);\n }\n } finally {\n if (windowId) {\n await this.safelyTerminateWindow(windowId);\n }\n }\n })();\n\n await this.activePromisesMutex.runExclusive(() => {\n this.activePromises.push(promise);\n });\n\n // Remove the promise from the active list when it resolves\n promise.finally(async () => {\n await this.activePromisesMutex.runExclusive(() => {\n const index = this.activePromises.indexOf(promise);\n if (index > -1) {\n this.activePromises.splice(index, 1);\n }\n });\n });\n }\n\n // Wait for all processes to complete\n await Promise.allSettled(this.activePromises);\n\n // Remove the halt listener\n this.runEmitter.removeListener(\"halt\", this.handleHaltEvent);\n\n return results;\n }\n\n private async handleErrorWithCallback({\n originalError,\n url,\n windowId,\n liveViewUrl,\n callback,\n }: {\n originalError: unknown;\n url: BatchOperationUrl;\n windowId?: string;\n liveViewUrl?: string;\n callback: (error: BatchOperationError) => Promise<void>;\n }): Promise<void> {\n // Catch any errors in the onError callback to avoid halting the entire process\n try {\n await callback({\n error: this.formatError(originalError),\n operationUrls: [url],\n sessionId: this.sessionId,\n windowId,\n liveViewUrl,\n });\n } catch (newError) {\n this.logger.error(\n `Error in onError callback: ${this.formatError(newError)}. Original error: ${this.formatError(originalError)}`,\n );\n }\n }\n\n private async safelyTerminateWindow(windowId: string): Promise<void> {\n try {\n await this.client.withSessionId(this.sessionId).withWindowId(windowId).close();\n } catch (error) {\n this.logger.withError(error).error(`Error closing window ${windowId}`);\n }\n }\n\n private formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n }\n\n private handleErrorAndWarningResponses({\n warnings,\n errors,\n sessionId,\n url,\n operation,\n }: { warnings?: Issue[]; errors?: Issue[]; sessionId: string; url: BatchOperationUrl; operation: string }): void {\n if (!warnings && !errors) return;\n\n const details: { sessionId: string; url: BatchOperationUrl; warnings?: Issue[]; errors?: Issue[] } = {\n sessionId,\n url,\n };\n\n if (warnings) {\n details.warnings = warnings;\n this.logger\n .withMetadata({\n details,\n })\n .warn(`Received warnings for ${operation}`);\n }\n\n if (errors) {\n details.errors = errors;\n this.logger\n .withMetadata({\n details,\n })\n .error(`Received errors for ${operation}`);\n }\n }\n}\n","import type { BatchOperationUrl } from \"./types.js\";\n\nexport const distributeUrlsToBatches = (\n urls: BatchOperationUrl[],\n maxConcurrentSessions: number,\n): BatchOperationUrl[][] => {\n if (urls.length === 0) return [];\n\n // Calculate optimal number of batches\n const batchCount = Math.min(maxConcurrentSessions, urls.length);\n const batches: BatchOperationUrl[][] = Array.from({ length: batchCount }, () => []);\n\n urls.forEach((url, index) => {\n const batchIndex = index % batchCount;\n if (!batches[batchIndex]) {\n batches[batchIndex] = [];\n }\n batches[batchIndex].push(url);\n });\n\n return batches;\n};\n"]}
@@ -0,0 +1,48 @@
1
+ import { CreateSessionConfig, AirtopPluginRegistration } from '@airtop/sdk';
2
+
3
+ interface BatchOperateConfig {
4
+ maxConcurrentSessions?: number;
5
+ maxWindowsPerSession?: number;
6
+ sessionConfig?: CreateSessionConfig;
7
+ onError?: (error: BatchOperationError) => Promise<void>;
8
+ }
9
+ interface BatchOperationUrl {
10
+ url: string;
11
+ context?: Record<string, unknown>;
12
+ }
13
+ interface BatchOperationInput {
14
+ windowId: string;
15
+ sessionId: string;
16
+ liveViewUrl: string;
17
+ operationUrl: BatchOperationUrl;
18
+ }
19
+ interface BatchOperationResponse<T> {
20
+ shouldHaltBatch?: boolean;
21
+ additionalUrls?: BatchOperationUrl[];
22
+ data?: T;
23
+ }
24
+ interface BatchOperationError {
25
+ error: Error | string;
26
+ operationUrls: BatchOperationUrl[];
27
+ sessionId?: string;
28
+ windowId?: string;
29
+ liveViewUrl?: string;
30
+ }
31
+
32
+ /**
33
+ * The methods provided by the Batch Operate plugin.
34
+ */
35
+ interface BatchOperateMethods {
36
+ batchOperate: <T>(urls: BatchOperationUrl[], operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>, config?: BatchOperateConfig) => Promise<T[]>;
37
+ }
38
+ declare module "@airtop/sdk" {
39
+ interface AirtopClient {
40
+ batchOperate<T>(urls: BatchOperationUrl[], operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>, config?: BatchOperateConfig): Promise<T[]>;
41
+ }
42
+ }
43
+ /**
44
+ * Use with registerAirtopPlugin() from the Airtop SDK to add this plugin.
45
+ */
46
+ declare function batchOperatePlugin(): AirtopPluginRegistration;
47
+
48
+ export { type BatchOperateConfig, type BatchOperateMethods, type BatchOperationError, type BatchOperationInput, type BatchOperationResponse, type BatchOperationUrl, batchOperatePlugin };
@@ -0,0 +1,48 @@
1
+ import { CreateSessionConfig, AirtopPluginRegistration } from '@airtop/sdk';
2
+
3
+ interface BatchOperateConfig {
4
+ maxConcurrentSessions?: number;
5
+ maxWindowsPerSession?: number;
6
+ sessionConfig?: CreateSessionConfig;
7
+ onError?: (error: BatchOperationError) => Promise<void>;
8
+ }
9
+ interface BatchOperationUrl {
10
+ url: string;
11
+ context?: Record<string, unknown>;
12
+ }
13
+ interface BatchOperationInput {
14
+ windowId: string;
15
+ sessionId: string;
16
+ liveViewUrl: string;
17
+ operationUrl: BatchOperationUrl;
18
+ }
19
+ interface BatchOperationResponse<T> {
20
+ shouldHaltBatch?: boolean;
21
+ additionalUrls?: BatchOperationUrl[];
22
+ data?: T;
23
+ }
24
+ interface BatchOperationError {
25
+ error: Error | string;
26
+ operationUrls: BatchOperationUrl[];
27
+ sessionId?: string;
28
+ windowId?: string;
29
+ liveViewUrl?: string;
30
+ }
31
+
32
+ /**
33
+ * The methods provided by the Batch Operate plugin.
34
+ */
35
+ interface BatchOperateMethods {
36
+ batchOperate: <T>(urls: BatchOperationUrl[], operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>, config?: BatchOperateConfig) => Promise<T[]>;
37
+ }
38
+ declare module "@airtop/sdk" {
39
+ interface AirtopClient {
40
+ batchOperate<T>(urls: BatchOperationUrl[], operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>, config?: BatchOperateConfig): Promise<T[]>;
41
+ }
42
+ }
43
+ /**
44
+ * Use with registerAirtopPlugin() from the Airtop SDK to add this plugin.
45
+ */
46
+ declare function batchOperatePlugin(): AirtopPluginRegistration;
47
+
48
+ export { type BatchOperateConfig, type BatchOperateMethods, type BatchOperationError, type BatchOperationInput, type BatchOperationResponse, type BatchOperationUrl, batchOperatePlugin };
package/dist/index.js ADDED
@@ -0,0 +1,509 @@
1
+ // src/batch-operate.plugin.ts
2
+ import { AirtopPluginAugmentationType } from "@airtop/sdk";
3
+
4
+ // src/lib/batch-util.ts
5
+ import { EventEmitter } from "eventemitter3";
6
+
7
+ // src/lib/SessionQueue.ts
8
+ import { Mutex as Mutex2 } from "async-mutex";
9
+
10
+ // src/lib/WindowQueue.ts
11
+ import { Mutex } from "async-mutex";
12
+ var WindowQueue = class {
13
+ activePromises = [];
14
+ urlQueue = [];
15
+ activePromisesMutex = new Mutex();
16
+ urlQueueMutex = new Mutex();
17
+ maxWindowsPerSession;
18
+ runEmitter;
19
+ sessionId;
20
+ client;
21
+ operation;
22
+ onError;
23
+ isHalted;
24
+ logger;
25
+ constructor(maxWindowsPerSession, runEmitter, sessionId, client, operation, onError, isHalted = false) {
26
+ if (!Number.isInteger(maxWindowsPerSession) || maxWindowsPerSession <= 0) {
27
+ throw new Error("maxWindowsPerSession must be a positive integer");
28
+ }
29
+ this.maxWindowsPerSession = maxWindowsPerSession;
30
+ this.runEmitter = runEmitter;
31
+ this.sessionId = sessionId;
32
+ this.client = client;
33
+ this.operation = operation;
34
+ this.onError = onError;
35
+ this.isHalted = isHalted;
36
+ this.logger = this.client.getLogger();
37
+ }
38
+ async addUrlToQueue(url) {
39
+ await this.urlQueueMutex.runExclusive(() => {
40
+ this.urlQueue.push(url);
41
+ });
42
+ }
43
+ handleHaltEvent() {
44
+ this.logger.info("Halt event received");
45
+ this.isHalted = true;
46
+ }
47
+ async processInBatches(urls) {
48
+ const results = [];
49
+ this.runEmitter.on("halt", this.handleHaltEvent.bind(this));
50
+ await this.urlQueueMutex.runExclusive(() => {
51
+ this.urlQueue = [...urls];
52
+ });
53
+ this.logger.withMetadata({
54
+ urls,
55
+ sessionId: this.sessionId
56
+ }).info(`Processing batch for session ${this.sessionId}`);
57
+ while (this.urlQueue.length > 0) {
58
+ let shouldContinue = false;
59
+ await this.activePromisesMutex.runExclusive(async () => {
60
+ if (this.activePromises.length >= this.maxWindowsPerSession) {
61
+ await Promise.race(this.activePromises);
62
+ shouldContinue = true;
63
+ }
64
+ });
65
+ if (shouldContinue) continue;
66
+ let urlData;
67
+ await this.urlQueueMutex.runExclusive(() => {
68
+ urlData = this.urlQueue.shift();
69
+ });
70
+ if (!urlData) break;
71
+ const promise = (async () => {
72
+ if (this.isHalted) {
73
+ this.logger.info(`Processing halted, skipping window creation for ${urlData.url}`);
74
+ return;
75
+ }
76
+ let windowId;
77
+ let liveViewUrl;
78
+ try {
79
+ this.logger.info(`Creating window for ${urlData.url} in session ${this.sessionId}`);
80
+ const { data, errors, warnings } = await this.client.withSessionId(this.sessionId).createWindow(urlData.url);
81
+ windowId = data.windowId;
82
+ this.handleErrorAndWarningResponses({
83
+ warnings,
84
+ errors,
85
+ sessionId: this.sessionId,
86
+ url: urlData,
87
+ operation: "window creation"
88
+ });
89
+ if (!windowId) {
90
+ throw new Error(`WindowId not found, errors: ${JSON.stringify(errors)}`);
91
+ }
92
+ const {
93
+ data: windowInfo,
94
+ warnings: windowWarnings,
95
+ errors: windowErrors
96
+ } = await this.client.withSessionId(this.sessionId).withWindowId(windowId).getWindowInfo();
97
+ liveViewUrl = windowInfo.liveViewUrl;
98
+ this.handleErrorAndWarningResponses({
99
+ warnings: windowWarnings,
100
+ errors: windowErrors,
101
+ sessionId: this.sessionId,
102
+ url: urlData,
103
+ operation: "window info retrieval"
104
+ });
105
+ const result = await this.operation({
106
+ windowId,
107
+ sessionId: this.sessionId,
108
+ liveViewUrl,
109
+ operationUrl: urlData
110
+ });
111
+ if (result) {
112
+ const { shouldHaltBatch, additionalUrls, data: data2 } = result;
113
+ if (data2) {
114
+ results.push(data2);
115
+ }
116
+ if (shouldHaltBatch) {
117
+ this.logger.info("Emitting halt event");
118
+ this.runEmitter.emit("halt");
119
+ }
120
+ if (additionalUrls && additionalUrls.length > 0) {
121
+ this.logger.withMetadata({
122
+ additionalUrls
123
+ }).info("Emitting addUrls event");
124
+ this.runEmitter.emit("addUrls", additionalUrls);
125
+ }
126
+ }
127
+ } catch (error) {
128
+ if (this.onError) {
129
+ await this.handleErrorWithCallback({
130
+ originalError: error,
131
+ url: urlData,
132
+ callback: this.onError,
133
+ windowId,
134
+ liveViewUrl
135
+ });
136
+ } else {
137
+ const message = `Error for URL ${urlData.url}: ${this.formatError(error)}`;
138
+ this.logger.error(message);
139
+ }
140
+ } finally {
141
+ if (windowId) {
142
+ await this.safelyTerminateWindow(windowId);
143
+ }
144
+ }
145
+ })();
146
+ await this.activePromisesMutex.runExclusive(() => {
147
+ this.activePromises.push(promise);
148
+ });
149
+ promise.finally(async () => {
150
+ await this.activePromisesMutex.runExclusive(() => {
151
+ const index = this.activePromises.indexOf(promise);
152
+ if (index > -1) {
153
+ this.activePromises.splice(index, 1);
154
+ }
155
+ });
156
+ });
157
+ }
158
+ await Promise.allSettled(this.activePromises);
159
+ this.runEmitter.removeListener("halt", this.handleHaltEvent);
160
+ return results;
161
+ }
162
+ async handleErrorWithCallback({
163
+ originalError,
164
+ url,
165
+ windowId,
166
+ liveViewUrl,
167
+ callback
168
+ }) {
169
+ try {
170
+ await callback({
171
+ error: this.formatError(originalError),
172
+ operationUrls: [url],
173
+ sessionId: this.sessionId,
174
+ windowId,
175
+ liveViewUrl
176
+ });
177
+ } catch (newError) {
178
+ this.logger.error(
179
+ `Error in onError callback: ${this.formatError(newError)}. Original error: ${this.formatError(originalError)}`
180
+ );
181
+ }
182
+ }
183
+ async safelyTerminateWindow(windowId) {
184
+ try {
185
+ await this.client.withSessionId(this.sessionId).withWindowId(windowId).close();
186
+ } catch (error) {
187
+ this.logger.withError(error).error(`Error closing window ${windowId}`);
188
+ }
189
+ }
190
+ formatError(error) {
191
+ return error instanceof Error ? error.message : String(error);
192
+ }
193
+ handleErrorAndWarningResponses({
194
+ warnings,
195
+ errors,
196
+ sessionId,
197
+ url,
198
+ operation
199
+ }) {
200
+ if (!warnings && !errors) return;
201
+ const details = {
202
+ sessionId,
203
+ url
204
+ };
205
+ if (warnings) {
206
+ details.warnings = warnings;
207
+ this.logger.withMetadata({
208
+ details
209
+ }).warn(`Received warnings for ${operation}`);
210
+ }
211
+ if (errors) {
212
+ details.errors = errors;
213
+ this.logger.withMetadata({
214
+ details
215
+ }).error(`Received errors for ${operation}`);
216
+ }
217
+ }
218
+ };
219
+
220
+ // src/lib/helpers.ts
221
+ var distributeUrlsToBatches = (urls, maxConcurrentSessions) => {
222
+ if (urls.length === 0) return [];
223
+ const batchCount = Math.min(maxConcurrentSessions, urls.length);
224
+ const batches = Array.from({ length: batchCount }, () => []);
225
+ urls.forEach((url, index) => {
226
+ const batchIndex = index % batchCount;
227
+ if (!batches[batchIndex]) {
228
+ batches[batchIndex] = [];
229
+ }
230
+ batches[batchIndex].push(url);
231
+ });
232
+ return batches;
233
+ };
234
+
235
+ // src/lib/SessionQueue.ts
236
+ var SessionQueue = class {
237
+ activePromises = [];
238
+ activePromisesMutex = new Mutex2();
239
+ maxConcurrentSessions;
240
+ runEmitter;
241
+ maxWindowsPerSession;
242
+ sessionConfig;
243
+ initialBatches = [];
244
+ operation;
245
+ onError;
246
+ isHalted;
247
+ batchQueue = [];
248
+ batchQueueMutex = new Mutex2();
249
+ latestProcessingPromise = null;
250
+ processingPromisesCount = 0;
251
+ client;
252
+ sessionPool = [];
253
+ sessionPoolMutex = new Mutex2();
254
+ results;
255
+ logger;
256
+ constructor({
257
+ maxConcurrentSessions,
258
+ runEmitter,
259
+ maxWindowsPerSession,
260
+ initialBatches,
261
+ operation,
262
+ client,
263
+ sessionConfig,
264
+ onError
265
+ }) {
266
+ if (!Number.isInteger(maxConcurrentSessions) || maxConcurrentSessions <= 0) {
267
+ throw new Error("maxConcurrentSessions must be a positive integer");
268
+ }
269
+ this.maxConcurrentSessions = maxConcurrentSessions;
270
+ this.runEmitter = runEmitter;
271
+ this.maxWindowsPerSession = maxWindowsPerSession;
272
+ this.sessionConfig = sessionConfig;
273
+ this.initialBatches = initialBatches;
274
+ this.operation = operation;
275
+ this.onError = onError;
276
+ this.latestProcessingPromise = null;
277
+ this.results = [];
278
+ this.client = client;
279
+ this.isHalted = false;
280
+ this.logger = client.getLogger();
281
+ }
282
+ handleHaltEvent() {
283
+ this.logger.info("Halt event received");
284
+ this.isHalted = true;
285
+ }
286
+ async addUrlsToBatchQueue(newBatch) {
287
+ const newBatches = distributeUrlsToBatches(newBatch, this.maxConcurrentSessions);
288
+ this.logger.withMetadata({
289
+ newBatches
290
+ }).info("Adding new batches to queue");
291
+ await this.batchQueueMutex.runExclusive(() => {
292
+ this.batchQueue.push(...newBatches);
293
+ });
294
+ this.processingPromisesCount++;
295
+ this.latestProcessingPromise = this.processPendingBatches();
296
+ }
297
+ async processInitialBatches() {
298
+ await this.batchQueueMutex.runExclusive(() => {
299
+ this.batchQueue = [...this.initialBatches];
300
+ });
301
+ this.processingPromisesCount++;
302
+ this.runEmitter.on("halt", this.handleHaltEvent.bind(this));
303
+ this.latestProcessingPromise = this.processPendingBatches();
304
+ await this.latestProcessingPromise;
305
+ }
306
+ async waitForProcessingToComplete() {
307
+ while (this.processingPromisesCount > 0) {
308
+ await this.latestProcessingPromise;
309
+ }
310
+ await this.terminateAllSessions();
311
+ this.runEmitter.removeListener("halt", this.handleHaltEvent);
312
+ return this.results;
313
+ }
314
+ async terminateAllSessions() {
315
+ for (const sessionId of this.sessionPool) {
316
+ this.safelyTerminateSession(sessionId);
317
+ }
318
+ }
319
+ async processPendingBatches() {
320
+ try {
321
+ while (this.batchQueue.length > 0) {
322
+ let shouldContinue = false;
323
+ await this.activePromisesMutex.runExclusive(async () => {
324
+ if (this.activePromises.length >= this.maxConcurrentSessions) {
325
+ await Promise.race(this.activePromises);
326
+ shouldContinue = true;
327
+ }
328
+ });
329
+ if (shouldContinue) continue;
330
+ let batch;
331
+ await this.batchQueueMutex.runExclusive(() => {
332
+ batch = this.batchQueue.shift();
333
+ });
334
+ if (!batch || batch.length === 0) break;
335
+ const promise = (async () => {
336
+ if (this.isHalted) {
337
+ this.logger.info("Halt event received, skipping batch");
338
+ return;
339
+ }
340
+ let sessionId;
341
+ try {
342
+ await this.sessionPoolMutex.runExclusive(() => {
343
+ if (this.sessionPool.length > 0) {
344
+ sessionId = this.sessionPool.pop();
345
+ }
346
+ });
347
+ if (!sessionId) {
348
+ const { data: session, warnings, errors } = await this.client.createSession(this.sessionConfig);
349
+ sessionId = session.id;
350
+ this.handleErrorAndWarningResponses({ warnings, errors, sessionId, batch });
351
+ }
352
+ const queue = new WindowQueue(
353
+ this.maxWindowsPerSession,
354
+ this.runEmitter,
355
+ sessionId,
356
+ this.client,
357
+ this.operation,
358
+ this.onError,
359
+ this.isHalted
360
+ );
361
+ const windowResults = await queue.processInBatches(batch);
362
+ this.results.push(...windowResults);
363
+ await this.sessionPoolMutex.runExclusive(() => {
364
+ if (!sessionId) {
365
+ throw new Error("Missing sessionId, cannot return to pool");
366
+ }
367
+ this.sessionPool.push(sessionId);
368
+ });
369
+ } catch (error) {
370
+ if (this.onError) {
371
+ await this.handleErrorWithCallback({ originalError: error, batch, sessionId, callback: this.onError });
372
+ } else {
373
+ const urls = batch.map((url) => url.url);
374
+ this.logErrorForUrls(urls, error);
375
+ }
376
+ if (sessionId) {
377
+ this.safelyTerminateSession(sessionId);
378
+ }
379
+ }
380
+ })();
381
+ await this.activePromisesMutex.runExclusive(() => {
382
+ this.activePromises.push(promise);
383
+ });
384
+ promise.finally(async () => {
385
+ await this.activePromisesMutex.runExclusive(() => {
386
+ const index = this.activePromises.indexOf(promise);
387
+ if (index > -1) {
388
+ this.activePromises.splice(index, 1);
389
+ }
390
+ });
391
+ });
392
+ }
393
+ await Promise.allSettled(this.activePromises);
394
+ } finally {
395
+ this.processingPromisesCount--;
396
+ }
397
+ }
398
+ async handleErrorWithCallback({
399
+ originalError,
400
+ batch,
401
+ sessionId,
402
+ callback
403
+ }) {
404
+ try {
405
+ await callback({
406
+ error: this.formatError(originalError),
407
+ operationUrls: batch,
408
+ sessionId
409
+ });
410
+ } catch (newError) {
411
+ this.logger.error(
412
+ `Error in onError callback: ${this.formatError(newError)}. Original error: ${this.formatError(originalError)}`
413
+ );
414
+ }
415
+ }
416
+ logErrorForUrls(urls, error) {
417
+ this.logger.withError(error).withMetadata({
418
+ urls
419
+ }).error("Error for URLs");
420
+ }
421
+ safelyTerminateSession(sessionId) {
422
+ this.client.withSessionId(sessionId).terminateSession().catch((error) => {
423
+ this.logger.withMetadata({
424
+ sessionId
425
+ }).withError(error).error(`Error terminating session ${sessionId}`);
426
+ });
427
+ }
428
+ formatError(error) {
429
+ return error instanceof Error ? error.message : String(error);
430
+ }
431
+ handleErrorAndWarningResponses({
432
+ warnings,
433
+ errors,
434
+ sessionId,
435
+ batch
436
+ }) {
437
+ if (!warnings && !errors) return;
438
+ const details = {
439
+ sessionId,
440
+ urls: batch
441
+ };
442
+ if (warnings) {
443
+ details.warnings = warnings;
444
+ this.logger.withMetadata({
445
+ details
446
+ }).warn("Received warnings creating session");
447
+ }
448
+ if (errors) {
449
+ details.errors = errors;
450
+ this.logger.withMetadata({ details }).error("Received errors creating session");
451
+ }
452
+ }
453
+ };
454
+
455
+ // src/lib/batch-util.ts
456
+ var DEFAULT_MAX_WINDOWS_PER_SESSION = 1;
457
+ var DEFAULT_MAX_CONCURRENT_SESSIONS = 30;
458
+ var batchOperate = async (urls, operation, client, config) => {
459
+ if (!Array.isArray(urls)) {
460
+ throw new Error("Please provide a valid list of urls");
461
+ }
462
+ for (const url of urls) {
463
+ if (!url || typeof url !== "object" || !("url" in url)) {
464
+ throw new Error("Please provide a valid list of urls");
465
+ }
466
+ }
467
+ const runEmitter = new EventEmitter();
468
+ const {
469
+ maxConcurrentSessions = DEFAULT_MAX_CONCURRENT_SESSIONS,
470
+ maxWindowsPerSession = DEFAULT_MAX_WINDOWS_PER_SESSION,
471
+ sessionConfig,
472
+ onError
473
+ } = config ?? {};
474
+ const initialBatches = distributeUrlsToBatches(urls, maxConcurrentSessions);
475
+ const sessionQueue = new SessionQueue({
476
+ maxConcurrentSessions,
477
+ runEmitter,
478
+ maxWindowsPerSession,
479
+ initialBatches,
480
+ operation,
481
+ client,
482
+ sessionConfig,
483
+ onError
484
+ });
485
+ runEmitter.on("addUrls", (additionalUrls) => {
486
+ sessionQueue.addUrlsToBatchQueue(additionalUrls);
487
+ });
488
+ await sessionQueue.processInitialBatches();
489
+ return await sessionQueue.waitForProcessingToComplete();
490
+ };
491
+
492
+ // src/batch-operate.plugin.ts
493
+ var plugin = {
494
+ augmentationType: AirtopPluginAugmentationType.AirtopClient,
495
+ augment: (prototype) => {
496
+ prototype.batchOperate = async function(urls, operation, config) {
497
+ return await batchOperate(urls, operation, this, config);
498
+ };
499
+ }
500
+ };
501
+ function batchOperatePlugin() {
502
+ return {
503
+ pluginsToAdd: [plugin]
504
+ };
505
+ }
506
+ export {
507
+ batchOperatePlugin
508
+ };
509
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/batch-operate.plugin.ts","../src/lib/batch-util.ts","../src/lib/SessionQueue.ts","../src/lib/WindowQueue.ts","../src/lib/helpers.ts"],"sourcesContent":["import { AirtopPluginAugmentationType } from \"@airtop/sdk\";\nimport type { AirtopPluginRegistration } from \"@airtop/sdk\";\nimport type { AirtopClientPlugin } from \"@airtop/sdk\";\nimport type { AirtopClient } from \"@airtop/sdk\";\nimport { batchOperate } from \"./lib/batch-util.js\";\nimport type {\n BatchOperateConfig,\n BatchOperationInput,\n BatchOperationResponse,\n BatchOperationUrl,\n} from \"./lib/types.js\";\n\n// This is exported so the docs can expose it as we can't generate docs against the\n// module declaration\n/**\n * The methods provided by the Batch Operate plugin.\n */\nexport interface BatchOperateMethods {\n batchOperate: <T>(\n urls: BatchOperationUrl[],\n operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>,\n config?: BatchOperateConfig,\n ) => Promise<T[]>;\n}\n\ndeclare module \"@airtop/sdk\" {\n interface AirtopClient {\n batchOperate<T>(\n urls: BatchOperationUrl[],\n operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>,\n config?: BatchOperateConfig,\n ): Promise<T[]>;\n }\n}\n\nconst plugin: AirtopClientPlugin = {\n augmentationType: AirtopPluginAugmentationType.AirtopClient,\n augment: (prototype: typeof AirtopClient.prototype) => {\n prototype.batchOperate = async function <T>(\n urls: BatchOperationUrl[],\n operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>,\n config?: BatchOperateConfig,\n ): Promise<T[]> {\n return await batchOperate(urls, operation, this, config);\n };\n },\n};\n\n/**\n * Use with registerAirtopPlugin() from the Airtop SDK to add this plugin.\n */\nexport function batchOperatePlugin(): AirtopPluginRegistration {\n return {\n pluginsToAdd: [plugin],\n };\n}\n","import type { AirtopClient } from \"@airtop/sdk\";\nimport { EventEmitter } from \"eventemitter3\";\nimport { SessionQueue } from \"./SessionQueue.js\";\nimport { distributeUrlsToBatches } from \"./helpers.js\";\nimport type { BatchOperateConfig, BatchOperationInput, BatchOperationResponse, BatchOperationUrl } from \"./types.js\";\n\nconst DEFAULT_MAX_WINDOWS_PER_SESSION = 1;\nconst DEFAULT_MAX_CONCURRENT_SESSIONS = 30;\n\nexport const batchOperate = async <T>(\n urls: BatchOperationUrl[],\n operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>, // operation to invoke on each url\n client: AirtopClient,\n config?: BatchOperateConfig,\n): Promise<T[]> => {\n // Validate the urls before proceeding\n if (!Array.isArray(urls)) {\n throw new Error(\"Please provide a valid list of urls\");\n }\n\n for (const url of urls) {\n if (!url || typeof url !== \"object\" || !(\"url\" in url)) {\n throw new Error(\"Please provide a valid list of urls\");\n }\n }\n\n const runEmitter = new EventEmitter();\n\n const {\n maxConcurrentSessions = DEFAULT_MAX_CONCURRENT_SESSIONS,\n maxWindowsPerSession = DEFAULT_MAX_WINDOWS_PER_SESSION,\n sessionConfig,\n onError,\n } = config ?? {};\n\n // Split the urls into batches\n const initialBatches = distributeUrlsToBatches(urls, maxConcurrentSessions);\n const sessionQueue = new SessionQueue({\n maxConcurrentSessions,\n runEmitter,\n maxWindowsPerSession,\n initialBatches,\n operation,\n client,\n sessionConfig,\n onError,\n });\n\n runEmitter.on(\"addUrls\", (additionalUrls: BatchOperationUrl[]) => {\n sessionQueue.addUrlsToBatchQueue(additionalUrls);\n });\n\n await sessionQueue.processInitialBatches();\n\n return await sessionQueue.waitForProcessingToComplete();\n};\n","import type { Issue } from \"@airtop/sdk\";\nimport type { AirtopClient, ILogLayer } from \"@airtop/sdk\";\nimport type { CreateSessionConfig } from \"@airtop/sdk\";\nimport { Mutex } from \"async-mutex\";\nimport type { EventEmitter } from \"eventemitter3\";\nimport { WindowQueue } from \"./WindowQueue.js\";\nimport { distributeUrlsToBatches } from \"./helpers.js\";\nimport type { BatchOperationError, BatchOperationInput, BatchOperationResponse, BatchOperationUrl } from \"./types.js\";\n\nexport class SessionQueue<T> {\n private activePromises: Promise<void>[] = [];\n private activePromisesMutex = new Mutex();\n\n private maxConcurrentSessions: number;\n private runEmitter: EventEmitter;\n private maxWindowsPerSession: number;\n private sessionConfig?: CreateSessionConfig;\n private initialBatches: BatchOperationUrl[][] = [];\n private operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>;\n private onError?: (error: BatchOperationError) => Promise<void>;\n private isHalted: boolean;\n\n private batchQueue: BatchOperationUrl[][] = [];\n private batchQueueMutex = new Mutex();\n private latestProcessingPromise: Promise<void> | null = null;\n private processingPromisesCount = 0;\n\n private client: AirtopClient;\n private sessionPool: string[] = [];\n private sessionPoolMutex = new Mutex();\n\n private results: T[];\n private logger: ILogLayer;\n\n constructor({\n maxConcurrentSessions,\n runEmitter,\n maxWindowsPerSession,\n initialBatches,\n operation,\n client,\n sessionConfig,\n onError,\n }: {\n maxConcurrentSessions: number;\n runEmitter: EventEmitter;\n maxWindowsPerSession: number;\n initialBatches: BatchOperationUrl[][];\n operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>;\n client: AirtopClient;\n sessionConfig?: CreateSessionConfig;\n onError?: (error: BatchOperationError) => Promise<void>;\n }) {\n if (!Number.isInteger(maxConcurrentSessions) || maxConcurrentSessions <= 0) {\n throw new Error(\"maxConcurrentSessions must be a positive integer\");\n }\n\n this.maxConcurrentSessions = maxConcurrentSessions;\n this.runEmitter = runEmitter;\n this.maxWindowsPerSession = maxWindowsPerSession;\n this.sessionConfig = sessionConfig;\n this.initialBatches = initialBatches;\n this.operation = operation;\n this.onError = onError;\n this.latestProcessingPromise = null;\n this.results = [];\n this.client = client;\n this.isHalted = false;\n this.logger = client.getLogger();\n }\n\n public handleHaltEvent(): void {\n this.logger.info(\"Halt event received\");\n this.isHalted = true;\n }\n\n public async addUrlsToBatchQueue(newBatch: BatchOperationUrl[]): Promise<void> {\n // Distribute new URLs across batches\n const newBatches = distributeUrlsToBatches(newBatch, this.maxConcurrentSessions);\n\n this.logger\n .withMetadata({\n newBatches,\n })\n .info(\"Adding new batches to queue\");\n\n // Add new batches to the queue\n await this.batchQueueMutex.runExclusive(() => {\n this.batchQueue.push(...newBatches);\n });\n\n // Update existing processing promise\n this.processingPromisesCount++;\n this.latestProcessingPromise = this.processPendingBatches();\n }\n\n public async processInitialBatches(): Promise<void> {\n await this.batchQueueMutex.runExclusive(() => {\n this.batchQueue = [...this.initialBatches];\n });\n this.processingPromisesCount++;\n this.runEmitter.on(\"halt\", this.handleHaltEvent.bind(this));\n this.latestProcessingPromise = this.processPendingBatches();\n await this.latestProcessingPromise;\n }\n\n public async waitForProcessingToComplete(): Promise<T[]> {\n while (this.processingPromisesCount > 0) {\n await this.latestProcessingPromise;\n }\n\n await this.terminateAllSessions();\n\n this.runEmitter.removeListener(\"halt\", this.handleHaltEvent);\n\n return this.results;\n }\n\n private async terminateAllSessions(): Promise<void> {\n for (const sessionId of this.sessionPool) {\n this.safelyTerminateSession(sessionId);\n }\n }\n\n private async processPendingBatches(): Promise<void> {\n try {\n while (this.batchQueue.length > 0) {\n // Wait for any session to complete before starting a new one\n let shouldContinue = false;\n await this.activePromisesMutex.runExclusive(async () => {\n if (this.activePromises.length >= this.maxConcurrentSessions) {\n await Promise.race(this.activePromises);\n shouldContinue = true;\n }\n });\n\n if (shouldContinue) continue;\n\n let batch: BatchOperationUrl[] | undefined;\n await this.batchQueueMutex.runExclusive(() => {\n batch = this.batchQueue.shift();\n });\n\n if (!batch || batch.length === 0) break;\n\n const promise = (async () => {\n if (this.isHalted) {\n this.logger.info(\"Halt event received, skipping batch\");\n return;\n }\n\n let sessionId: string | undefined;\n try {\n // Check if there's an available session in the pool\n await this.sessionPoolMutex.runExclusive(() => {\n if (this.sessionPool.length > 0) {\n sessionId = this.sessionPool.pop();\n }\n });\n\n // Otherwise, create a new session\n if (!sessionId) {\n const { data: session, warnings, errors } = await this.client.createSession(this.sessionConfig);\n sessionId = session.id;\n\n this.handleErrorAndWarningResponses({ warnings, errors, sessionId, batch });\n }\n\n const queue = new WindowQueue(\n this.maxWindowsPerSession,\n this.runEmitter,\n sessionId,\n this.client,\n this.operation,\n this.onError,\n this.isHalted,\n );\n const windowResults = await queue.processInBatches(batch);\n this.results.push(...windowResults);\n\n // Return the session to the pool\n await this.sessionPoolMutex.runExclusive(() => {\n if (!sessionId) {\n throw new Error(\"Missing sessionId, cannot return to pool\");\n }\n\n this.sessionPool.push(sessionId);\n });\n } catch (error) {\n if (this.onError) {\n await this.handleErrorWithCallback({ originalError: error, batch, sessionId, callback: this.onError });\n } else {\n // By default, log the error and continue\n const urls = batch.map((url) => url.url);\n this.logErrorForUrls(urls, error);\n }\n\n // Clean up the session in case of error\n if (sessionId) {\n this.safelyTerminateSession(sessionId);\n }\n }\n })();\n\n await this.activePromisesMutex.runExclusive(() => {\n this.activePromises.push(promise);\n });\n\n // Remove the promise when it completes\n promise.finally(async () => {\n await this.activePromisesMutex.runExclusive(() => {\n const index = this.activePromises.indexOf(promise);\n if (index > -1) {\n this.activePromises.splice(index, 1);\n }\n });\n });\n }\n\n // Wait for all remaining sessions to complete\n await Promise.allSettled(this.activePromises);\n } finally {\n this.processingPromisesCount--;\n }\n }\n\n private async handleErrorWithCallback({\n originalError,\n batch,\n sessionId,\n callback,\n }: {\n originalError: unknown;\n batch: BatchOperationUrl[];\n sessionId?: string;\n callback: (error: BatchOperationError) => Promise<void>;\n }): Promise<void> {\n // Catch any errors in the onError callback to avoid halting the entire process\n try {\n await callback({\n error: this.formatError(originalError),\n operationUrls: batch,\n sessionId,\n });\n } catch (newError) {\n this.logger.error(\n `Error in onError callback: ${this.formatError(newError)}. Original error: ${this.formatError(originalError)}`,\n );\n }\n }\n\n private logErrorForUrls(urls: string[], error: unknown): void {\n this.logger\n .withError(error)\n .withMetadata({\n urls,\n })\n .error(\"Error for URLs\");\n }\n\n private safelyTerminateSession(sessionId: string): void {\n // Do not await since we don't want to block the main thread\n this.client\n .withSessionId(sessionId)\n .terminateSession()\n .catch((error) => {\n this.logger\n .withMetadata({\n sessionId,\n })\n .withError(error)\n .error(`Error terminating session ${sessionId}`);\n });\n }\n\n private formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n }\n\n private handleErrorAndWarningResponses({\n warnings,\n errors,\n sessionId,\n batch,\n }: { warnings?: Issue[]; errors?: Issue[]; sessionId: string; batch: BatchOperationUrl[] }): void {\n if (!warnings && !errors) return;\n\n const details: { sessionId: string; urls: BatchOperationUrl[]; warnings?: Issue[]; errors?: Issue[] } = {\n sessionId,\n urls: batch,\n };\n\n if (warnings) {\n details.warnings = warnings;\n this.logger\n .withMetadata({\n details,\n })\n .warn(\"Received warnings creating session\");\n }\n\n // Log an object with the errors and the URL\n if (errors) {\n details.errors = errors;\n this.logger.withMetadata({ details }).error(\"Received errors creating session\");\n }\n }\n}\n","import type { Issue } from \"@airtop/sdk\";\nimport type { AirtopClient } from \"@airtop/sdk\";\nimport type { ILogLayer } from \"@airtop/sdk\";\nimport { Mutex } from \"async-mutex\";\nimport type { EventEmitter } from \"eventemitter3\";\nimport type { BatchOperationError, BatchOperationInput, BatchOperationResponse, BatchOperationUrl } from \"./types.js\";\n\nexport class WindowQueue<T> {\n private activePromises: Promise<void>[] = [];\n private urlQueue: BatchOperationUrl[] = [];\n private activePromisesMutex = new Mutex();\n private urlQueueMutex = new Mutex();\n\n private maxWindowsPerSession: number;\n private runEmitter: EventEmitter;\n private sessionId: string;\n private client: AirtopClient;\n private operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>;\n private onError?: (error: BatchOperationError) => Promise<void>;\n private isHalted;\n private logger: ILogLayer;\n\n constructor(\n maxWindowsPerSession: number,\n runEmitter: EventEmitter,\n sessionId: string,\n client: AirtopClient,\n operation: (input: BatchOperationInput) => Promise<BatchOperationResponse<T>>,\n onError?: (error: BatchOperationError) => Promise<void>,\n isHalted = false,\n ) {\n if (!Number.isInteger(maxWindowsPerSession) || maxWindowsPerSession <= 0) {\n throw new Error(\"maxWindowsPerSession must be a positive integer\");\n }\n\n this.maxWindowsPerSession = maxWindowsPerSession;\n this.runEmitter = runEmitter;\n this.sessionId = sessionId;\n this.client = client;\n this.operation = operation;\n this.onError = onError;\n this.isHalted = isHalted;\n this.logger = this.client.getLogger();\n }\n\n public async addUrlToQueue(url: BatchOperationUrl): Promise<void> {\n await this.urlQueueMutex.runExclusive(() => {\n this.urlQueue.push(url);\n });\n }\n\n private handleHaltEvent(): void {\n this.logger.info(\"Halt event received\");\n this.isHalted = true;\n }\n\n async processInBatches(urls: BatchOperationUrl[]): Promise<T[]> {\n const results: T[] = [];\n this.runEmitter.on(\"halt\", this.handleHaltEvent.bind(this));\n\n await this.urlQueueMutex.runExclusive(() => {\n this.urlQueue = [...urls];\n });\n this.logger\n .withMetadata({\n urls,\n sessionId: this.sessionId,\n })\n .info(`Processing batch for session ${this.sessionId}`);\n\n while (this.urlQueue.length > 0) {\n // Wait for any window to complete before starting a new one\n let shouldContinue = false;\n await this.activePromisesMutex.runExclusive(async () => {\n if (this.activePromises.length >= this.maxWindowsPerSession) {\n await Promise.race(this.activePromises);\n shouldContinue = true;\n }\n });\n\n if (shouldContinue) continue;\n\n let urlData: BatchOperationUrl | undefined;\n await this.urlQueueMutex.runExclusive(() => {\n urlData = this.urlQueue.shift(); // Take the next url from the queue\n });\n if (!urlData) break; // No more urls to process\n\n // If we have less than the max concurrent operations, start a new one\n const promise = (async () => {\n // Do not process any more urls if the processing has been halted\n if (this.isHalted) {\n this.logger.info(`Processing halted, skipping window creation for ${urlData.url}`);\n return;\n }\n\n let windowId: string | undefined;\n let liveViewUrl: string | undefined;\n try {\n // Create a new window pointed to the url\n this.logger.info(`Creating window for ${urlData.url} in session ${this.sessionId}`);\n const { data, errors, warnings } = await this.client.withSessionId(this.sessionId).createWindow(urlData.url);\n\n windowId = data.windowId;\n\n this.handleErrorAndWarningResponses({\n warnings,\n errors,\n sessionId: this.sessionId,\n url: urlData,\n operation: \"window creation\",\n });\n\n if (!windowId) {\n throw new Error(`WindowId not found, errors: ${JSON.stringify(errors)}`);\n }\n\n const {\n data: windowInfo,\n warnings: windowWarnings,\n errors: windowErrors,\n } = await this.client.withSessionId(this.sessionId).withWindowId(windowId).getWindowInfo();\n liveViewUrl = windowInfo.liveViewUrl;\n\n this.handleErrorAndWarningResponses({\n warnings: windowWarnings,\n errors: windowErrors,\n sessionId: this.sessionId,\n url: urlData,\n operation: \"window info retrieval\",\n });\n\n // Run the operation on the window\n const result = await this.operation({\n windowId,\n sessionId: this.sessionId,\n liveViewUrl,\n operationUrl: urlData,\n });\n\n if (result) {\n const { shouldHaltBatch, additionalUrls, data } = result;\n\n if (data) {\n results.push(data);\n }\n\n if (shouldHaltBatch) {\n this.logger.info(\"Emitting halt event\");\n this.runEmitter.emit(\"halt\");\n }\n\n if (additionalUrls && additionalUrls.length > 0) {\n this.logger\n .withMetadata({\n additionalUrls,\n })\n .info(\"Emitting addUrls event\");\n this.runEmitter.emit(\"addUrls\", additionalUrls);\n }\n }\n } catch (error) {\n if (this.onError) {\n await this.handleErrorWithCallback({\n originalError: error,\n url: urlData,\n callback: this.onError,\n windowId,\n liveViewUrl,\n });\n } else {\n // By default, log the error and continue\n const message = `Error for URL ${urlData.url}: ${this.formatError(error)}`;\n this.logger.error(message);\n }\n } finally {\n if (windowId) {\n await this.safelyTerminateWindow(windowId);\n }\n }\n })();\n\n await this.activePromisesMutex.runExclusive(() => {\n this.activePromises.push(promise);\n });\n\n // Remove the promise from the active list when it resolves\n promise.finally(async () => {\n await this.activePromisesMutex.runExclusive(() => {\n const index = this.activePromises.indexOf(promise);\n if (index > -1) {\n this.activePromises.splice(index, 1);\n }\n });\n });\n }\n\n // Wait for all processes to complete\n await Promise.allSettled(this.activePromises);\n\n // Remove the halt listener\n this.runEmitter.removeListener(\"halt\", this.handleHaltEvent);\n\n return results;\n }\n\n private async handleErrorWithCallback({\n originalError,\n url,\n windowId,\n liveViewUrl,\n callback,\n }: {\n originalError: unknown;\n url: BatchOperationUrl;\n windowId?: string;\n liveViewUrl?: string;\n callback: (error: BatchOperationError) => Promise<void>;\n }): Promise<void> {\n // Catch any errors in the onError callback to avoid halting the entire process\n try {\n await callback({\n error: this.formatError(originalError),\n operationUrls: [url],\n sessionId: this.sessionId,\n windowId,\n liveViewUrl,\n });\n } catch (newError) {\n this.logger.error(\n `Error in onError callback: ${this.formatError(newError)}. Original error: ${this.formatError(originalError)}`,\n );\n }\n }\n\n private async safelyTerminateWindow(windowId: string): Promise<void> {\n try {\n await this.client.withSessionId(this.sessionId).withWindowId(windowId).close();\n } catch (error) {\n this.logger.withError(error).error(`Error closing window ${windowId}`);\n }\n }\n\n private formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n }\n\n private handleErrorAndWarningResponses({\n warnings,\n errors,\n sessionId,\n url,\n operation,\n }: { warnings?: Issue[]; errors?: Issue[]; sessionId: string; url: BatchOperationUrl; operation: string }): void {\n if (!warnings && !errors) return;\n\n const details: { sessionId: string; url: BatchOperationUrl; warnings?: Issue[]; errors?: Issue[] } = {\n sessionId,\n url,\n };\n\n if (warnings) {\n details.warnings = warnings;\n this.logger\n .withMetadata({\n details,\n })\n .warn(`Received warnings for ${operation}`);\n }\n\n if (errors) {\n details.errors = errors;\n this.logger\n .withMetadata({\n details,\n })\n .error(`Received errors for ${operation}`);\n }\n }\n}\n","import type { BatchOperationUrl } from \"./types.js\";\n\nexport const distributeUrlsToBatches = (\n urls: BatchOperationUrl[],\n maxConcurrentSessions: number,\n): BatchOperationUrl[][] => {\n if (urls.length === 0) return [];\n\n // Calculate optimal number of batches\n const batchCount = Math.min(maxConcurrentSessions, urls.length);\n const batches: BatchOperationUrl[][] = Array.from({ length: batchCount }, () => []);\n\n urls.forEach((url, index) => {\n const batchIndex = index % batchCount;\n if (!batches[batchIndex]) {\n batches[batchIndex] = [];\n }\n batches[batchIndex].push(url);\n });\n\n return batches;\n};\n"],"mappings":"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","names":["Mutex","data","Mutex"]}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@airtop/plugin-batch-operate",
3
+ "description": "Adds batch operation support to the Airtop SDK",
4
+ "version": "1.0.0-alpha2.0",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "exports": {
9
+ "import": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "require": {
14
+ "types": "./dist/index.d.cts",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "types": "./dist/index.d.ts",
19
+ "license": "MIT",
20
+ "repository": "airtop-ai/airtop-sdk.git",
21
+ "author": "Theo Gravity <theo@airtop.ai>",
22
+ "keywords": [
23
+ "airtop",
24
+ "batch",
25
+ "plugin"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsup src/index.ts",
29
+ "build:dev": "node_modules/.bin/hash-runner",
30
+ "clean": "rm -rf .turbo node_modules dist",
31
+ "lint": "biome check --write --unsafe src && biome format src --write && biome lint src --fix",
32
+ "verify-types": "tsc --noEmit"
33
+ },
34
+ "dependencies": {
35
+ "async-mutex": "0.5.0",
36
+ "eventemitter3": "5.0.1"
37
+ },
38
+ "devDependencies": {
39
+ "@biomejs/biome": "1.9.4",
40
+ "@internal/tsconfig": "workspace:*",
41
+ "hash-runner": "2.0.1",
42
+ "tsup": "8.4.0",
43
+ "typescript": "5.8.2",
44
+ "vitest": "3.1.1"
45
+ },
46
+ "peerDependencies": {
47
+ "@airtop/sdk": "workspace:*"
48
+ },
49
+ "bugs": "https://github.com/airtop-ai/airtop-sdk/issues",
50
+ "engines": {
51
+ "node": ">=18"
52
+ },
53
+ "files": [
54
+ "dist"
55
+ ],
56
+ "homepage": "https://airtop.ai",
57
+ "packageManager": "pnpm@10.5.0"
58
+ }