@signaliz/sdk 1.0.21 → 1.0.23

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.
@@ -0,0 +1,594 @@
1
+ // src/errors.ts
2
+ var SignalizError = class extends Error {
3
+ constructor(data) {
4
+ super(data.message);
5
+ this.name = "SignalizError";
6
+ this.code = data.code;
7
+ this.errorType = data.errorType;
8
+ this.retryAfter = data.retryAfter;
9
+ this.details = data.details;
10
+ }
11
+ get isRetryable() {
12
+ return this.errorType === "rate_limited" || this.errorType === "timeout" || this.errorType === "provider_error";
13
+ }
14
+ };
15
+ function parseError(status, body) {
16
+ if (body?.error && typeof body.error === "object") {
17
+ return new SignalizError({
18
+ code: body.error.code || `HTTP_${status}`,
19
+ message: body.error.message || `Request failed with status ${status}`,
20
+ errorType: mapErrorType(body.error.code, status),
21
+ retryAfter: body.error.retry_after,
22
+ details: body.error.details
23
+ });
24
+ }
25
+ return new SignalizError({
26
+ code: `HTTP_${status}`,
27
+ message: body?.message || body?.error || `Request failed with status ${status}`,
28
+ errorType: mapErrorType(void 0, status)
29
+ });
30
+ }
31
+ function mapErrorType(code, status) {
32
+ if (code === "RATE_LIMITED" || status === 429) return "rate_limited";
33
+ if (code === "VALIDATION_ERROR" || status === 400) return "validation";
34
+ if (code === "NOT_FOUND" || status === 404) return "not_found";
35
+ if (code === "AUTH_EXPIRED" || status === 401) return "auth_expired";
36
+ if (code === "INSUFFICIENT_CREDITS" || status === 402) return "insufficient_credits";
37
+ if (code === "TIMEOUT" || status === 504) return "timeout";
38
+ if (status >= 500) return "provider_error";
39
+ return "unknown";
40
+ }
41
+
42
+ // src/client.ts
43
+ var DEFAULT_BASE_URL = "https://api.signaliz.com/functions/v1";
44
+ var DEFAULT_TIMEOUT = 12e4;
45
+ var DEFAULT_MAX_RETRIES = 3;
46
+ var HttpClient = class {
47
+ constructor(config) {
48
+ this.baseUrl = config.baseUrl?.replace(/\/$/, "") || DEFAULT_BASE_URL;
49
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
50
+ this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
51
+ this.apiKey = config.apiKey;
52
+ this.clientId = config.clientId;
53
+ this.clientSecret = config.clientSecret;
54
+ if (!this.apiKey && !(this.clientId && this.clientSecret)) {
55
+ throw new Error("Signaliz: provide either apiKey or clientId + clientSecret");
56
+ }
57
+ }
58
+ async request(functionName, body, method = "POST") {
59
+ let lastError;
60
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
61
+ let timer;
62
+ try {
63
+ const token = await this.getToken();
64
+ const url = new URL(`${this.baseUrl}/${functionName}`);
65
+ if (method === "GET") {
66
+ for (const [key, value] of Object.entries(body)) {
67
+ if (value === void 0 || value === null) continue;
68
+ url.searchParams.set(key, String(value));
69
+ }
70
+ }
71
+ const controller = new AbortController();
72
+ timer = setTimeout(() => controller.abort(), this.timeout);
73
+ const init = {
74
+ method,
75
+ headers: {
76
+ "Content-Type": "application/json",
77
+ Authorization: `Bearer ${token}`
78
+ },
79
+ signal: controller.signal
80
+ };
81
+ if (method === "POST") {
82
+ init.body = JSON.stringify(body);
83
+ }
84
+ const res = await fetch(url, init);
85
+ if (!res.ok) {
86
+ const errBody = await res.json().catch(() => ({}));
87
+ const err = parseError(res.status, errBody);
88
+ if (err.isRetryable && attempt < this.maxRetries) {
89
+ const wait = err.retryAfter ? err.retryAfter * 1e3 : Math.min(1e3 * Math.pow(2, attempt), 1e4);
90
+ await sleep(wait);
91
+ lastError = err;
92
+ continue;
93
+ }
94
+ throw err;
95
+ }
96
+ return await res.json();
97
+ } catch (e) {
98
+ if (e instanceof SignalizError) throw e;
99
+ if (e.name === "AbortError") {
100
+ const timeout = new SignalizError({
101
+ code: "TIMEOUT",
102
+ message: `Request timed out after ${this.timeout}ms`,
103
+ errorType: "timeout"
104
+ });
105
+ if (attempt < this.maxRetries) {
106
+ lastError = timeout;
107
+ await sleep(1e3 * Math.pow(2, attempt));
108
+ continue;
109
+ }
110
+ throw timeout;
111
+ }
112
+ lastError = e;
113
+ if (attempt < this.maxRetries) {
114
+ await sleep(1e3 * Math.pow(2, attempt));
115
+ continue;
116
+ }
117
+ } finally {
118
+ if (timer) clearTimeout(timer);
119
+ }
120
+ }
121
+ throw lastError || new Error("Request failed after retries");
122
+ }
123
+ /** Convenience wrapper for POST-based edge function calls */
124
+ async post(functionName, body) {
125
+ return this.request(functionName, body, "POST");
126
+ }
127
+ /** Convenience wrapper for GET-based edge function calls with query params */
128
+ async get(functionName, query = {}) {
129
+ return this.request(functionName, query, "GET");
130
+ }
131
+ /** JSON-RPC call to the MCP server */
132
+ async mcp(method, params = {}) {
133
+ let lastError;
134
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
135
+ let timer;
136
+ try {
137
+ const token = await this.getToken();
138
+ const url = `${this.baseUrl}/signaliz-mcp`;
139
+ const controller = new AbortController();
140
+ timer = setTimeout(() => controller.abort(), this.timeout);
141
+ const res = await fetch(url, {
142
+ method: "POST",
143
+ headers: {
144
+ "Content-Type": "application/json",
145
+ Authorization: `Bearer ${token}`
146
+ },
147
+ body: JSON.stringify({
148
+ jsonrpc: "2.0",
149
+ id: Date.now(),
150
+ method,
151
+ params: normalizeMcpParams(method, params)
152
+ }),
153
+ signal: controller.signal
154
+ });
155
+ const text = await res.text();
156
+ const data = safeJson(text);
157
+ if (!res.ok) {
158
+ const err = parseError(res.status, data ?? { message: text });
159
+ if (err.isRetryable && attempt < this.maxRetries) {
160
+ lastError = err;
161
+ await sleep(err.retryAfter ? err.retryAfter * 1e3 : Math.min(1e3 * Math.pow(2, attempt), 1e4));
162
+ continue;
163
+ }
164
+ throw err;
165
+ }
166
+ if (!data) {
167
+ throw new SignalizError({
168
+ code: "INVALID_JSON",
169
+ message: "MCP response was not valid JSON",
170
+ errorType: "provider_error",
171
+ details: { responseText: text.slice(0, 500) }
172
+ });
173
+ }
174
+ const responseError = isRecord(data) && isRecord(data.error) ? data.error : null;
175
+ if (responseError) {
176
+ const err = new SignalizError({
177
+ code: firstString(responseError.code, isRecord(responseError.data) ? responseError.data.error_code : void 0) || "MCP_ERROR",
178
+ message: firstString(responseError.message) || "MCP request failed",
179
+ errorType: mapMcpErrorType(responseError),
180
+ retryAfter: isRecord(responseError.data) && typeof responseError.data.retry_after === "number" ? responseError.data.retry_after : void 0,
181
+ details: isRecord(responseError.data) ? responseError.data : void 0
182
+ });
183
+ if (err.isRetryable && attempt < this.maxRetries) {
184
+ lastError = err;
185
+ await sleep(err.retryAfter ? err.retryAfter * 1e3 : Math.min(1e3 * Math.pow(2, attempt), 1e4));
186
+ continue;
187
+ }
188
+ throw err;
189
+ }
190
+ return unwrapMcpResponse(data);
191
+ } catch (e) {
192
+ if (e instanceof SignalizError && !e.isRetryable) throw e;
193
+ if (e.name === "AbortError") {
194
+ lastError = new SignalizError({
195
+ code: "TIMEOUT",
196
+ message: `MCP request timed out after ${this.timeout}ms`,
197
+ errorType: "timeout"
198
+ });
199
+ } else {
200
+ lastError = e;
201
+ }
202
+ if (attempt < this.maxRetries) {
203
+ await sleep(Math.min(1e3 * Math.pow(2, attempt), 1e4));
204
+ continue;
205
+ }
206
+ } finally {
207
+ if (timer) clearTimeout(timer);
208
+ }
209
+ }
210
+ throw lastError || new Error("MCP request failed after retries");
211
+ }
212
+ async getToken() {
213
+ if (this.apiKey) return this.apiKey;
214
+ if (this.accessToken) return this.accessToken;
215
+ if (this.accessTokenPromise) return this.accessTokenPromise;
216
+ this.accessTokenPromise = (async () => {
217
+ const res = await fetch("https://api.signaliz.com/token", {
218
+ method: "POST",
219
+ headers: { "Content-Type": "application/json" },
220
+ body: JSON.stringify({
221
+ grant_type: "client_credentials",
222
+ client_id: this.clientId,
223
+ client_secret: this.clientSecret
224
+ })
225
+ });
226
+ if (!res.ok) {
227
+ throw new SignalizError({
228
+ code: "AUTH_FAILED",
229
+ message: "Failed to obtain access token",
230
+ errorType: "auth_expired"
231
+ });
232
+ }
233
+ const data = await res.json();
234
+ this.accessToken = data.access_token;
235
+ return this.accessToken;
236
+ })().finally(() => {
237
+ this.accessTokenPromise = void 0;
238
+ });
239
+ return this.accessTokenPromise;
240
+ }
241
+ };
242
+ function sleep(ms) {
243
+ return new Promise((r) => setTimeout(r, ms));
244
+ }
245
+ function safeJson(text) {
246
+ try {
247
+ return JSON.parse(text);
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
252
+ function normalizeMcpParams(method, params) {
253
+ if (method !== "tools/call") return params;
254
+ const args = params.arguments;
255
+ if (!args || typeof args !== "object" || Array.isArray(args)) return params;
256
+ return {
257
+ ...params,
258
+ arguments: {
259
+ output_format: "json",
260
+ ...args
261
+ }
262
+ };
263
+ }
264
+ function isRecord(value) {
265
+ return typeof value === "object" && value !== null;
266
+ }
267
+ function firstString(...values) {
268
+ const value = values.find((item) => typeof item === "string" && item.length > 0);
269
+ return typeof value === "string" ? value : void 0;
270
+ }
271
+ function firstPayloadError(payload) {
272
+ const errors = payload.errors;
273
+ const first = Array.isArray(errors) ? errors[0] : void 0;
274
+ return isRecord(first) ? first : {};
275
+ }
276
+ function unwrapMcpResponse(data) {
277
+ const result = isRecord(data) ? data.result : void 0;
278
+ if (isRecord(result) && result.structuredContent !== void 0) {
279
+ return unwrapMcpPayload(result.structuredContent);
280
+ }
281
+ const content = isRecord(result) ? result.content : void 0;
282
+ const firstContent = Array.isArray(content) ? content[0] : void 0;
283
+ const text = isRecord(firstContent) ? firstContent.text : void 0;
284
+ if (typeof text === "string") {
285
+ const parsed = safeJson(text);
286
+ return unwrapMcpPayload(parsed ?? text);
287
+ }
288
+ return unwrapMcpPayload(result);
289
+ }
290
+ function unwrapMcpPayload(payload) {
291
+ if (isRecord(payload)) {
292
+ if (payload.ok === true && Object.prototype.hasOwnProperty.call(payload, "result")) {
293
+ return unwrapMcpPayload(payload.result);
294
+ }
295
+ if (payload.ok === false) {
296
+ const first = firstPayloadError(payload);
297
+ const code = firstString(first.code, payload.error_code, payload.code) || (payload.approval_required ? "APPROVAL_REQUIRED" : "MCP_ERROR");
298
+ throw new SignalizError({
299
+ code,
300
+ message: firstString(first.message, payload.message, payload.summary, payload.error) || "MCP request failed",
301
+ errorType: mapErrorTypeFromCode(code),
302
+ details: {
303
+ ...payload,
304
+ ...isRecord(first.details) ? first.details : {}
305
+ }
306
+ });
307
+ }
308
+ if (payload.success === true && Object.prototype.hasOwnProperty.call(payload, "data")) {
309
+ return payload.data;
310
+ }
311
+ if (payload.success === false) {
312
+ const first = firstPayloadError(payload);
313
+ const code = firstString(first.code, payload.error_code) || (payload.approval_required ? "APPROVAL_REQUIRED" : "MCP_ERROR");
314
+ throw new SignalizError({
315
+ code,
316
+ message: firstString(first.message, payload.message, payload.summary) || "MCP request failed",
317
+ errorType: mapErrorTypeFromCode(code),
318
+ details: {
319
+ ...payload,
320
+ ...isRecord(first.details) ? first.details : {}
321
+ }
322
+ });
323
+ }
324
+ }
325
+ return payload;
326
+ }
327
+ function mapMcpErrorType(error) {
328
+ const errorRecord = isRecord(error) ? error : {};
329
+ const data = isRecord(errorRecord.data) ? errorRecord.data : {};
330
+ const code = String(data.error_code || errorRecord.code || "");
331
+ const message = String(errorRecord.message || "").toLowerCase();
332
+ if (code === "429" || message.includes("rate limit")) return "rate_limited";
333
+ if (code === "401" || code === "AUTH_001" || message.includes("auth")) return "auth_expired";
334
+ if (code === "400" || code === "-32602" || message.includes("invalid")) return "validation";
335
+ if (code === "404" || message.includes("not found")) return "not_found";
336
+ if (message.includes("timeout") || message.includes("timed out")) return "timeout";
337
+ return "unknown";
338
+ }
339
+ function mapErrorTypeFromCode(code) {
340
+ if (!code) return "unknown";
341
+ if (code === "RATE_LIMITED") return "rate_limited";
342
+ if (code === "INVALID_ARGUMENT" || code === "VALIDATION_ERROR" || code === "VALIDATION_001" || code === "APPROVAL_REQUIRED" || code === "WEBHOOK_APPROVAL_REQUIRED" || code === "TARGET_LIMIT_EXCEEDED") return "validation";
343
+ if (code === "AUTH_REQUIRED" || code === "AUTH_001") return "auth_expired";
344
+ if (code === "QUOTA_EXCEEDED") return "insufficient_credits";
345
+ if (code === "BUILD_NOT_READY" || code === "ARTIFACT_NOT_READY") return "not_found";
346
+ if (code === "BUILD_FAILED" || code === "INTERNAL_ERROR") return "provider_error";
347
+ return "unknown";
348
+ }
349
+
350
+ // src/index.ts
351
+ var Signaliz = class {
352
+ constructor(config) {
353
+ this.client = new HttpClient(config);
354
+ }
355
+ async findEmail(params) {
356
+ const data = await this.client.post("api/v1/find-email", compact({
357
+ company_domain: params.companyDomain,
358
+ full_name: params.fullName,
359
+ first_name: params.firstName,
360
+ last_name: params.lastName,
361
+ linkedin_url: params.linkedinUrl,
362
+ company_name: params.companyName,
363
+ skip_cache: params.skipCache
364
+ }));
365
+ const email = typeof data.email === "string" && data.email.trim() ? data.email.trim() : null;
366
+ const found = Boolean(email) && data.found !== false;
367
+ return {
368
+ success: data.success !== false && found,
369
+ found,
370
+ email,
371
+ firstName: data.first_name,
372
+ lastName: data.last_name,
373
+ confidence: data.confidence ?? data.confidence_score ?? 0,
374
+ verificationStatus: data.verification_status ?? data.status ?? "unknown",
375
+ providerUsed: data.provider_used ?? data.provider ?? data.source ?? "unknown",
376
+ error: found ? void 0 : data.error ?? "No verified email found",
377
+ errorCode: found ? void 0 : data.error_code ?? "EMAIL_NOT_FOUND",
378
+ raw: data
379
+ };
380
+ }
381
+ async findEmails(params, options) {
382
+ return runBatch(params, (item) => this.findEmail(item), options);
383
+ }
384
+ async verifyEmail(email) {
385
+ const data = await this.client.post("api/v1/verify-email", { email });
386
+ const verificationStatus = String(data.verification_status ?? "").toLowerCase();
387
+ const verified = data.email_verified === true || ["valid", "deliverable"].includes(verificationStatus);
388
+ return {
389
+ email: data.email ?? email,
390
+ isValid: data.is_valid ?? data.valid ?? verified,
391
+ isDeliverable: data.is_deliverable ?? data.deliverable ?? verified,
392
+ isCatchAll: data.is_catch_all ?? data.catch_all ?? data.diagnostics?.is_catch_all ?? false,
393
+ confidenceScore: data.confidence_score ?? data.confidence ?? data.diagnostics?.confidence_score ?? (verified ? 1 : 0),
394
+ provider: data.provider,
395
+ verificationSource: data.verification_source,
396
+ raw: data
397
+ };
398
+ }
399
+ async verifyEmails(emails, options) {
400
+ return runBatch(emails, (email) => this.verifyEmail(email), options);
401
+ }
402
+ async enrichCompanySignals(params) {
403
+ const online = params.online ?? true;
404
+ const request = compact({
405
+ company_domain: params.companyDomain,
406
+ company_name: params.companyName,
407
+ research_prompt: params.researchPrompt,
408
+ signal_types: params.signalTypes,
409
+ target_signal_count: params.targetSignalCount,
410
+ lookback_days: params.lookbackDays,
411
+ online,
412
+ enable_deep_search: params.enableDeepSearch ?? online,
413
+ enable_outreach_intelligence: params.enableOutreachIntelligence,
414
+ enable_predictive_intelligence: params.enablePredictiveIntelligence,
415
+ skip_cache: params.skipCache,
416
+ wait_for_result: params.waitForResult !== false,
417
+ max_wait_ms: params.maxWaitMs
418
+ });
419
+ let data = await this.client.post("api/v1/company-signals", request);
420
+ if (params.waitForResult !== false && data.run_id && isPendingSignalResponse(data)) {
421
+ const maxWaitMs = Math.max(1e3, params.maxWaitMs ?? 20 * 6e4);
422
+ const pollIntervalMs = Math.max(250, params.pollIntervalMs ?? 2e3);
423
+ const startedAt = Date.now();
424
+ while (Date.now() - startedAt < maxWaitMs) {
425
+ const status = await this.client.post("api/v1/company-signals", {
426
+ ...request,
427
+ signal_run_id: data.run_id
428
+ });
429
+ if (isCompletedSignalRun(status)) {
430
+ data = status.output ?? status.result ?? status.data ?? status;
431
+ break;
432
+ }
433
+ if (isFailedSignalRun(status)) {
434
+ throw new Error(`Company signal enrichment ${data.run_id} failed with status ${status.status || "unknown"}`);
435
+ }
436
+ if (!isPendingSignalResponse(status)) {
437
+ data = status.output ?? status.result ?? status.data ?? status;
438
+ break;
439
+ }
440
+ await sleep2(pollIntervalMs);
441
+ }
442
+ if (isPendingSignalResponse(data)) {
443
+ throw new Error(`Company signal enrichment ${data.run_id} did not complete within ${maxWaitMs}ms`);
444
+ }
445
+ }
446
+ return {
447
+ success: data.success ?? true,
448
+ company: {
449
+ name: data.company?.name ?? params.companyName ?? null,
450
+ domain: data.company?.domain ?? params.companyDomain ?? null
451
+ },
452
+ signals: (data.signals ?? []).map((signal) => ({
453
+ id: signal.id,
454
+ title: signal.title ?? "",
455
+ type: signal.type ?? signal.signal_type ?? "unknown",
456
+ content: signal.content ?? signal.description ?? "",
457
+ date: signal.date ?? signal.detected_at ?? null,
458
+ sourceUrl: signal.source_url ?? signal.url ?? null,
459
+ sourceType: signal.source_type,
460
+ confidence: signal.confidence ?? signal.confidence_score ?? null
461
+ })),
462
+ signalCount: data.signal_count ?? data.total_signals ?? data.signals?.length ?? 0,
463
+ intelligence: data.intelligence ?? null,
464
+ credits: data.credits,
465
+ metadata: data.metadata,
466
+ raw: data
467
+ };
468
+ }
469
+ async enrichCompanies(companies, options) {
470
+ return runBatch(companies, (company) => this.enrichCompanySignals(company), options);
471
+ }
472
+ async signalToCopy(params) {
473
+ const request = compact({
474
+ company_domain: params.companyDomain,
475
+ person_name: params.personName,
476
+ title: params.title,
477
+ campaign_offer: params.campaignOffer,
478
+ research_prompt: params.researchPrompt,
479
+ signal_run_id: params.signalRunId
480
+ });
481
+ let data = await this.client.post("api/v1/signal-to-copy", request);
482
+ if (params.waitForResult !== false && data.run_id && isPendingSignalResponse(data)) {
483
+ const maxWaitMs = Math.max(1e3, params.maxWaitMs ?? 20 * 6e4);
484
+ const pollIntervalMs = Math.max(250, params.pollIntervalMs ?? 2e3);
485
+ const startedAt = Date.now();
486
+ while (Date.now() - startedAt < maxWaitMs) {
487
+ await sleep2(pollIntervalMs);
488
+ data = await this.client.post("api/v1/signal-to-copy", {
489
+ ...request,
490
+ signal_run_id: data.run_id
491
+ });
492
+ if (!isPendingSignalResponse(data)) break;
493
+ }
494
+ if (isPendingSignalResponse(data)) {
495
+ throw new Error(`Signal Copy ${data.run_id} did not complete within ${maxWaitMs}ms`);
496
+ }
497
+ }
498
+ return {
499
+ success: data.success ?? true,
500
+ status: data.status,
501
+ runId: data.run_id,
502
+ strongestSignal: data.strongest_signal ?? "",
503
+ whyNow: data.why_now ?? "",
504
+ subjectLine: data.subject_line ?? "",
505
+ openingLine: data.opening_line ?? "",
506
+ confidence: data.confidence ?? 0,
507
+ evidenceUrl: data.evidence_url ?? "",
508
+ researchPrompt: data.research_prompt,
509
+ variations: (data.variations ?? []).map((variation) => ({
510
+ style: variation.style,
511
+ subjectLine: variation.subject_line ?? "",
512
+ openingLine: variation.opening_line ?? "",
513
+ cta: variation.cta ?? "",
514
+ prediction: variation.prediction,
515
+ email: variation.email
516
+ })),
517
+ raw: data
518
+ };
519
+ }
520
+ async createSignalCopyBatch(requests, options) {
521
+ return runBatch(requests, (request) => this.signalToCopy(request), options);
522
+ }
523
+ async listTools() {
524
+ const result = await this.client.mcp("tools/list");
525
+ return result.tools ?? [];
526
+ }
527
+ async health() {
528
+ const result = await this.client.get("api/v1/health");
529
+ return {
530
+ status: result.status ?? "unknown",
531
+ products: Array.isArray(result.products) ? result.products : []
532
+ };
533
+ }
534
+ };
535
+ function compact(input) {
536
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== void 0));
537
+ }
538
+ function isPendingSignalResponse(data) {
539
+ return ["triggered", "queued", "processing", "pending", "pending_version", "waiting_for_deploy"].includes(String(data.status || "").toLowerCase());
540
+ }
541
+ function isCompletedSignalRun(data) {
542
+ return ["completed", "success", "succeeded"].includes(String(data.status || "").toLowerCase());
543
+ }
544
+ function isFailedSignalRun(data) {
545
+ return ["failed", "error", "cancelled", "canceled", "crashed", "timed_out"].includes(String(data.status || "").toLowerCase());
546
+ }
547
+ function sleep2(ms) {
548
+ return new Promise((resolve) => setTimeout(resolve, ms));
549
+ }
550
+ async function runBatch(items, execute, options) {
551
+ if (!Array.isArray(items) || items.length === 0) {
552
+ throw new Error("Signaliz batch requests require at least one item");
553
+ }
554
+ if (items.length > 5e3) {
555
+ throw new Error(`Signaliz batch requests support at most 5,000 items; received ${items.length}`);
556
+ }
557
+ const concurrency = options?.concurrency ?? 10;
558
+ if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 50) {
559
+ throw new Error("Signaliz batch concurrency must be an integer between 1 and 50");
560
+ }
561
+ const startedAt = Date.now();
562
+ const results = new Array(items.length);
563
+ let nextIndex = 0;
564
+ const worker = async () => {
565
+ while (true) {
566
+ const index = nextIndex;
567
+ nextIndex += 1;
568
+ if (index >= items.length) return;
569
+ try {
570
+ results[index] = { index, success: true, data: await execute(items[index]) };
571
+ } catch (error) {
572
+ results[index] = {
573
+ index,
574
+ success: false,
575
+ error: error instanceof Error ? error.message : String(error)
576
+ };
577
+ }
578
+ }
579
+ };
580
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
581
+ const succeeded = results.filter((item) => item.success).length;
582
+ return {
583
+ total: items.length,
584
+ succeeded,
585
+ failed: items.length - succeeded,
586
+ durationMs: Date.now() - startedAt,
587
+ results
588
+ };
589
+ }
590
+
591
+ export {
592
+ SignalizError,
593
+ Signaliz
594
+ };