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.
- package/dist/factories.cjs +280 -29
- package/dist/factories.cjs.map +1 -1
- package/dist/factories.d.cts +43 -83
- package/dist/factories.d.ts +43 -83
- package/dist/factories.js +280 -29
- package/dist/factories.js.map +1 -1
- package/dist/index.cjs +132 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +132 -18
- package/dist/index.js.map +1 -1
- package/dist/middleware.cjs +248 -127
- package/dist/middleware.cjs.map +1 -1
- package/dist/middleware.d.cts +175 -126
- package/dist/middleware.d.ts +175 -126
- package/dist/middleware.js +241 -128
- package/dist/middleware.js.map +1 -1
- package/dist/webhook.cjs +132 -18
- package/dist/webhook.cjs.map +1 -1
- package/dist/webhook.d.cts +8 -27
- package/dist/webhook.d.ts +8 -27
- package/dist/webhook.js +132 -18
- package/dist/webhook.js.map +1 -1
- package/package.json +1 -1
- package/src/factories.compose.test.ts +107 -0
- package/src/factories.ts +240 -95
- package/src/http-client.ts +127 -0
- package/src/middleware.test.ts +115 -0
- package/src/middleware.ts +449 -302
- package/src/retry-classification.ts +78 -0
- package/src/webhook.test.ts +102 -219
- package/src/webhook.ts +57 -47
package/dist/factories.cjs
CHANGED
|
@@ -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.
|
|
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 =
|
|
15655
|
-
|
|
15656
|
-
|
|
15657
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
16168
|
-
async trackEvent(
|
|
16169
|
-
await
|
|
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
|
|
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
|
|
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(
|
|
16178
|
-
await
|
|
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(
|
|
16432
|
+
await Promise.all(subscribers.map(async (subscriber) => subscriber.shutdown?.()));
|
|
16182
16433
|
}
|
|
16183
16434
|
};
|
|
16184
16435
|
}
|