autotel-subscribers 31.0.4 → 31.1.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.
@@ -15636,43 +15636,157 @@ var SegmentSubscriber = class {
15636
15636
  }
15637
15637
  };
15638
15638
 
15639
+ // src/http-client.ts
15640
+ async function parseBody(response) {
15641
+ const text = await response.text();
15642
+ if (text.trim().length === 0) return null;
15643
+ try {
15644
+ return JSON.parse(text);
15645
+ } catch {
15646
+ return text;
15647
+ }
15648
+ }
15649
+ function isTimeoutError(error) {
15650
+ if (!(error instanceof Error)) return false;
15651
+ return error.name === "AbortError" || error.name === "TimeoutError";
15652
+ }
15653
+ function createHttpClient(options = {}) {
15654
+ const defaultTimeoutMs = options.timeoutMs ?? 3e4;
15655
+ return {
15656
+ async request(url, requestOptions = {}) {
15657
+ const timeoutMs = requestOptions.timeoutMs ?? defaultTimeoutMs;
15658
+ const controller = new AbortController();
15659
+ const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
15660
+ try {
15661
+ const response = await fetch(url, {
15662
+ method: requestOptions.method ?? "GET",
15663
+ headers: requestOptions.headers,
15664
+ body: requestOptions.body,
15665
+ signal: controller.signal
15666
+ });
15667
+ if (!response.ok) {
15668
+ const body = await parseBody(response);
15669
+ return {
15670
+ ok: false,
15671
+ kind: "http",
15672
+ status: response.status,
15673
+ statusText: response.statusText,
15674
+ body
15675
+ };
15676
+ }
15677
+ const data = await parseBody(response);
15678
+ return { ok: true, status: response.status, data };
15679
+ } catch (error) {
15680
+ const cause = error instanceof Error ? error : new Error(String(error));
15681
+ return {
15682
+ ok: false,
15683
+ kind: "network",
15684
+ timedOut: isTimeoutError(error),
15685
+ cause
15686
+ };
15687
+ } finally {
15688
+ clearTimeout(timeoutHandle);
15689
+ }
15690
+ }
15691
+ };
15692
+ }
15693
+
15694
+ // src/retry-classification.ts
15695
+ var SubscriberProviderError = class extends Error {
15696
+ code;
15697
+ retriable;
15698
+ details;
15699
+ constructor(options) {
15700
+ super(options.message, options.cause ? { cause: options.cause } : void 0);
15701
+ this.name = "SubscriberProviderError";
15702
+ this.code = options.code;
15703
+ this.retriable = options.retriable;
15704
+ this.details = options.details;
15705
+ }
15706
+ };
15707
+ function mapHttpStatus(status) {
15708
+ switch (status) {
15709
+ case 400:
15710
+ case 422: {
15711
+ return { code: "VALIDATION", retriable: false };
15712
+ }
15713
+ case 401:
15714
+ case 403:
15715
+ case 404: {
15716
+ return { code: "CONFIG", retriable: false };
15717
+ }
15718
+ case 429: {
15719
+ return { code: "RATE_LIMITED", retriable: true };
15720
+ }
15721
+ default: {
15722
+ return { code: "PROVIDER", retriable: status >= 500 };
15723
+ }
15724
+ }
15725
+ }
15726
+ function isProviderRetriable(error) {
15727
+ if (error instanceof SubscriberProviderError) return error.retriable;
15728
+ return true;
15729
+ }
15730
+
15639
15731
  // src/webhook.ts
15640
15732
  var WebhookSubscriber = class {
15641
15733
  name = "WebhookSubscriber";
15642
- version = "1.0.0";
15734
+ version = "1.1.0";
15643
15735
  config;
15644
15736
  enabled;
15645
15737
  pendingRequests = /* @__PURE__ */ new Set();
15738
+ httpClient;
15646
15739
  constructor(config) {
15647
15740
  this.config = config;
15648
15741
  this.enabled = config.enabled ?? true;
15742
+ this.httpClient = createHttpClient({ timeoutMs: config.timeoutMs });
15743
+ }
15744
+ async delay(ms) {
15745
+ await new Promise((resolve) => setTimeout(resolve, ms));
15649
15746
  }
15650
15747
  async send(payload) {
15651
15748
  if (!this.enabled) return;
15652
15749
  const maxRetries = this.config.maxRetries ?? 3;
15750
+ const retryDelayMs = this.config.retryDelayMs ?? 1e3;
15751
+ const method = this.config.method ?? "POST";
15653
15752
  let lastError;
15654
- for (let attempt2 = 0; attempt2 < maxRetries; attempt2++) {
15655
- try {
15656
- const response = await fetch(this.config.url, {
15657
- method: "POST",
15753
+ for (let attempt2 = 1; attempt2 <= maxRetries; attempt2++) {
15754
+ const response = await this.httpClient.request(
15755
+ this.config.url,
15756
+ {
15757
+ method,
15658
15758
  headers: {
15659
15759
  "Content-Type": "application/json",
15660
15760
  ...this.config.headers
15661
15761
  },
15662
- body: JSON.stringify(payload)
15663
- });
15664
- if (!response.ok) {
15665
- throw new Error(`Webhook returned ${response.status}: ${response.statusText}`);
15666
- }
15667
- return;
15668
- } catch (error) {
15669
- lastError = error;
15670
- if (attempt2 < maxRetries - 1) {
15671
- await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempt2) * 1e3));
15762
+ body: JSON.stringify(payload),
15763
+ timeoutMs: this.config.timeoutMs
15672
15764
  }
15765
+ );
15766
+ if (response.ok) return;
15767
+ if (response.kind === "network") {
15768
+ lastError = new SubscriberProviderError({
15769
+ message: response.timedOut ? "Webhook request timed out" : "Webhook network request failed",
15770
+ code: "NETWORK",
15771
+ retriable: true,
15772
+ details: response.cause,
15773
+ cause: response.cause
15774
+ });
15775
+ } else {
15776
+ const mapped = mapHttpStatus(response.status);
15777
+ lastError = new SubscriberProviderError({
15778
+ message: `Webhook returned ${response.status}: ${response.statusText}`,
15779
+ code: mapped.code,
15780
+ retriable: mapped.retriable,
15781
+ details: response.body
15782
+ });
15673
15783
  }
15784
+ const canRetry = isProviderRetriable(lastError) && attempt2 < maxRetries;
15785
+ if (!canRetry) break;
15786
+ const backoffMs = retryDelayMs * 2 ** (attempt2 - 1);
15787
+ await this.delay(backoffMs);
15674
15788
  }
15675
- console.error(`Webhook subscriber failed after ${maxRetries} attempts:`, lastError);
15789
+ throw lastError ?? new Error("Webhook send failed");
15676
15790
  }
15677
15791
  async trackEvent(name, attributes, options) {
15678
15792
  const request = this.send({
@@ -15723,11 +15837,11 @@ var WebhookSubscriber = class {
15723
15837
  }
15724
15838
  trackRequest(request) {
15725
15839
  this.pendingRequests.add(request);
15726
- void request.finally(() => {
15840
+ void request.catch(() => {
15841
+ }).finally(() => {
15727
15842
  this.pendingRequests.delete(request);
15728
15843
  });
15729
15844
  }
15730
- /** Wait for all pending webhook requests to complete */
15731
15845
  async shutdown() {
15732
15846
  if (this.pendingRequests.size > 0) {
15733
15847
  await Promise.allSettled(this.pendingRequests);
@@ -16162,23 +16276,160 @@ function createSlackSubscriber(config) {
16162
16276
  function createMockSubscriber() {
16163
16277
  return new MockEventSubscriber();
16164
16278
  }
16165
- function composeSubscribers(adapters) {
16279
+ function backoffDelay(attempt2, initialMs, maxMs) {
16280
+ return Math.min(maxMs, initialMs * 2 ** (attempt2 - 1));
16281
+ }
16282
+ async function callSubscriber(subscriber, call) {
16283
+ switch (call.method) {
16284
+ case "trackEvent": {
16285
+ await subscriber.trackEvent(
16286
+ call.args[0],
16287
+ call.args[1],
16288
+ call.args[2]
16289
+ );
16290
+ return;
16291
+ }
16292
+ case "trackFunnelStep": {
16293
+ await subscriber.trackFunnelStep(
16294
+ call.args[0],
16295
+ call.args[1],
16296
+ call.args[2],
16297
+ call.args[3]
16298
+ );
16299
+ return;
16300
+ }
16301
+ case "trackOutcome": {
16302
+ await subscriber.trackOutcome(
16303
+ call.args[0],
16304
+ call.args[1],
16305
+ call.args[2],
16306
+ call.args[3]
16307
+ );
16308
+ return;
16309
+ }
16310
+ case "trackValue": {
16311
+ await subscriber.trackValue(
16312
+ call.args[0],
16313
+ call.args[1],
16314
+ call.args[2],
16315
+ call.args[3]
16316
+ );
16317
+ }
16318
+ }
16319
+ }
16320
+ function composeSubscribers(subscribers, options = {}) {
16321
+ const strategy = options.strategy ?? "parallel";
16322
+ const name = options.name ?? `ComposedSubscriber(${strategy})`;
16323
+ const maxAttempts = options.maxAttemptsPerSubscriber ?? 1;
16324
+ const initialRetryDelayMs = options.initialRetryDelayMs ?? 250;
16325
+ const maxRetryDelayMs = options.maxRetryDelayMs ?? 1e4;
16326
+ const isRetriable = options.isRetriable ?? (() => true);
16327
+ const logger = options.logger ?? console;
16328
+ let rrCounter = 0;
16329
+ const sendOne = async (subscriber, call) => {
16330
+ let lastError;
16331
+ for (let attempt2 = 1; attempt2 <= maxAttempts; attempt2++) {
16332
+ try {
16333
+ await callSubscriber(subscriber, call);
16334
+ return;
16335
+ } catch (error) {
16336
+ lastError = error;
16337
+ const retryable = isRetriable(error);
16338
+ logger.warn?.("composeSubscribers attempt failed", {
16339
+ subscriber: subscriber.name,
16340
+ attempt: attempt2,
16341
+ retryable,
16342
+ strategy,
16343
+ error
16344
+ });
16345
+ if (!retryable || attempt2 >= maxAttempts) break;
16346
+ await new Promise(
16347
+ (resolve) => setTimeout(resolve, backoffDelay(attempt2, initialRetryDelayMs, maxRetryDelayMs))
16348
+ );
16349
+ }
16350
+ }
16351
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
16352
+ };
16353
+ const orderForSequential = () => {
16354
+ const n = subscribers.length;
16355
+ if (n === 0) return [];
16356
+ if (strategy === "failover") {
16357
+ return Array.from({ length: n }, (_, i) => i);
16358
+ }
16359
+ const start = strategy === "random" ? Math.floor(Math.random() * n) : rrCounter++ % n;
16360
+ return Array.from({ length: n }, (_, i) => (start + i) % n);
16361
+ };
16362
+ const execute = async (call) => {
16363
+ if (subscribers.length === 0) return;
16364
+ if (strategy === "parallel") {
16365
+ await Promise.all(subscribers.map((subscriber) => sendOne(subscriber, call)));
16366
+ return;
16367
+ }
16368
+ if (strategy === "race") {
16369
+ const attempts = subscribers.map(async (subscriber) => {
16370
+ await sendOne(subscriber, call);
16371
+ return subscriber.name ?? "unknown";
16372
+ });
16373
+ try {
16374
+ await Promise.any(attempts);
16375
+ } catch (error) {
16376
+ if (error instanceof AggregateError && error.errors.length > 0) {
16377
+ throw error.errors.at(-1);
16378
+ }
16379
+ throw error;
16380
+ }
16381
+ return;
16382
+ }
16383
+ if (strategy === "mirrored") {
16384
+ const primary = subscribers[0];
16385
+ if (!primary) return;
16386
+ await sendOne(primary, call);
16387
+ for (const mirror of subscribers.slice(1)) {
16388
+ void sendOne(mirror, call).catch((error) => {
16389
+ logger.warn?.("composeSubscribers mirror failed", {
16390
+ subscriber: mirror.name,
16391
+ error
16392
+ });
16393
+ });
16394
+ }
16395
+ return;
16396
+ }
16397
+ const order = orderForSequential();
16398
+ let lastError;
16399
+ for (const index of order) {
16400
+ const subscriber = subscribers[index];
16401
+ if (!subscriber) continue;
16402
+ try {
16403
+ await sendOne(subscriber, call);
16404
+ return;
16405
+ } catch (error) {
16406
+ lastError = error;
16407
+ }
16408
+ }
16409
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
16410
+ };
16166
16411
  return {
16167
- name: "ComposedSubscriber",
16168
- async trackEvent(name, attributes) {
16169
- await Promise.all(adapters.map((a) => a.trackEvent(name, attributes)));
16412
+ name,
16413
+ async trackEvent(name_, attributes, options_) {
16414
+ await execute({ method: "trackEvent", args: [name_, attributes, options_] });
16170
16415
  },
16171
- async trackFunnelStep(funnel, step, attributes) {
16172
- await Promise.all(adapters.map((a) => a.trackFunnelStep(funnel, step, attributes)));
16416
+ async trackFunnelStep(funnel, step, attributes, options_) {
16417
+ await execute({
16418
+ method: "trackFunnelStep",
16419
+ args: [funnel, step, attributes, options_]
16420
+ });
16173
16421
  },
16174
- async trackOutcome(operation, outcome, attributes) {
16175
- await Promise.all(adapters.map((a) => a.trackOutcome(operation, outcome, attributes)));
16422
+ async trackOutcome(operation, outcome, attributes, options_) {
16423
+ await execute({
16424
+ method: "trackOutcome",
16425
+ args: [operation, outcome, attributes, options_]
16426
+ });
16176
16427
  },
16177
- async trackValue(name, value, attributes) {
16178
- await Promise.all(adapters.map((a) => a.trackValue(name, value, attributes)));
16428
+ async trackValue(name_, value, attributes, options_) {
16429
+ await execute({ method: "trackValue", args: [name_, value, attributes, options_] });
16179
16430
  },
16180
16431
  async shutdown() {
16181
- await Promise.all(adapters.map((a) => a.shutdown?.()));
16432
+ await Promise.all(subscribers.map(async (subscriber) => subscriber.shutdown?.()));
16182
16433
  }
16183
16434
  };
16184
16435
  }