@tinybirdco/sdk 0.0.57 → 0.0.58
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/README.md +15 -0
- package/dist/api/api.d.ts +9 -0
- package/dist/api/api.d.ts.map +1 -1
- package/dist/api/api.js +155 -11
- package/dist/api/api.js.map +1 -1
- package/dist/api/api.test.js +254 -0
- package/dist/api/api.test.js.map +1 -1
- package/dist/client/types.d.ts +5 -0
- package/dist/client/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api/api.test.ts +389 -0
- package/src/api/api.ts +199 -12
- package/src/client/types.ts +5 -0
package/src/api/api.ts
CHANGED
|
@@ -14,6 +14,8 @@ import type {
|
|
|
14
14
|
} from "../client/types.js";
|
|
15
15
|
|
|
16
16
|
const DEFAULT_TIMEOUT = 30000;
|
|
17
|
+
const DEFAULT_INGEST_RETRY_503_BASE_DELAY_MS = 200;
|
|
18
|
+
const DEFAULT_INGEST_RETRY_503_MAX_DELAY_MS = 3000;
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Public, decoupled Tinybird API wrapper configuration
|
|
@@ -279,22 +281,49 @@ export class TinybirdApi {
|
|
|
279
281
|
const ndjson = events
|
|
280
282
|
.map((event) => JSON.stringify(this.serializeEvent(event)))
|
|
281
283
|
.join("\n");
|
|
284
|
+
const signal = this.createAbortSignal(options.timeout, options.signal);
|
|
285
|
+
const maxRetries = this.resolveIngestMaxRetries(options.maxRetries);
|
|
286
|
+
let retryCount = 0;
|
|
287
|
+
|
|
288
|
+
while (true) {
|
|
289
|
+
let response: Response;
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
response = await this.request(url.toString(), {
|
|
293
|
+
method: "POST",
|
|
294
|
+
token: options.token,
|
|
295
|
+
headers: {
|
|
296
|
+
"Content-Type": "application/x-ndjson",
|
|
297
|
+
},
|
|
298
|
+
body: ndjson,
|
|
299
|
+
signal,
|
|
300
|
+
});
|
|
301
|
+
} catch (error) {
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
282
304
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
305
|
+
if (response.ok) {
|
|
306
|
+
return (await response.json()) as IngestResult;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const retry429Delay = this.resolveRetry429Delay(response, maxRetries, retryCount);
|
|
310
|
+
if (retry429Delay !== undefined) {
|
|
311
|
+
await this.discardResponseBody(response);
|
|
312
|
+
await this.sleep(retry429Delay, signal);
|
|
313
|
+
retryCount += 1;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const retry503Delay = this.resolveRetry503Delay(response, maxRetries, retryCount);
|
|
318
|
+
if (retry503Delay !== undefined) {
|
|
319
|
+
await this.discardResponseBody(response);
|
|
320
|
+
await this.sleep(retry503Delay, signal);
|
|
321
|
+
retryCount += 1;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
292
324
|
|
|
293
|
-
if (!response.ok) {
|
|
294
325
|
await this.handleErrorResponse(response);
|
|
295
326
|
}
|
|
296
|
-
|
|
297
|
-
return (await response.json()) as IngestResult;
|
|
298
327
|
}
|
|
299
328
|
|
|
300
329
|
/**
|
|
@@ -575,6 +604,164 @@ export class TinybirdApi {
|
|
|
575
604
|
return AbortSignal.any([timeoutSignal, existingSignal]);
|
|
576
605
|
}
|
|
577
606
|
|
|
607
|
+
private resolveIngestMaxRetries(
|
|
608
|
+
maxRetries: TinybirdApiIngestOptions["maxRetries"]
|
|
609
|
+
): number | undefined {
|
|
610
|
+
if (maxRetries === undefined) {
|
|
611
|
+
return undefined;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (!Number.isFinite(maxRetries)) {
|
|
615
|
+
throw new Error("'maxRetries' must be a finite number");
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return Math.max(0, Math.floor(maxRetries));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
private resolveRetry429Delay(
|
|
622
|
+
response: Response,
|
|
623
|
+
maxRetries: number | undefined,
|
|
624
|
+
retryCount: number
|
|
625
|
+
): number | undefined {
|
|
626
|
+
if (maxRetries === undefined) {
|
|
627
|
+
return undefined;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (response.status !== 429) {
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (retryCount >= maxRetries) {
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return this.resolveRetryDelayFromHeaders(response);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private resolveRetry503Delay(
|
|
642
|
+
response: Response,
|
|
643
|
+
maxRetries: number | undefined,
|
|
644
|
+
retryCount: number
|
|
645
|
+
): number | undefined {
|
|
646
|
+
if (maxRetries === undefined) {
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (response.status !== 503) {
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (retryCount >= maxRetries) {
|
|
655
|
+
return undefined;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return this.calculateRetry503DelayMs(retryCount);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private resolveRetryDelayFromHeaders(response: Response): number | undefined {
|
|
662
|
+
const retryAfter = response.headers.get("retry-after");
|
|
663
|
+
const retryAfterDelay = this.parseRetryAfterDelayMs(retryAfter);
|
|
664
|
+
if (retryAfterDelay !== undefined) {
|
|
665
|
+
return retryAfterDelay;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const rateLimitReset = response.headers.get("x-ratelimit-reset");
|
|
669
|
+
const rateLimitResetDelay = this.parseRateLimitResetDelayMs(rateLimitReset);
|
|
670
|
+
if (rateLimitResetDelay !== undefined) {
|
|
671
|
+
return rateLimitResetDelay;
|
|
672
|
+
}
|
|
673
|
+
return undefined;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private parseRetryAfterDelayMs(value: string | null): number | undefined {
|
|
677
|
+
if (!value) {
|
|
678
|
+
return undefined;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const trimmed = value.trim();
|
|
682
|
+
const seconds = Number(trimmed);
|
|
683
|
+
if (Number.isFinite(seconds)) {
|
|
684
|
+
return Math.max(0, Math.floor(seconds * 1000));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const retryDateMs = Date.parse(trimmed);
|
|
688
|
+
if (Number.isNaN(retryDateMs)) {
|
|
689
|
+
return undefined;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return Math.max(0, retryDateMs - Date.now());
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private parseRateLimitResetDelayMs(value: string | null): number | undefined {
|
|
696
|
+
if (!value) {
|
|
697
|
+
return undefined;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const numericValue = Number(value.trim());
|
|
701
|
+
if (!Number.isFinite(numericValue)) {
|
|
702
|
+
return undefined;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return Math.max(0, Math.floor(numericValue * 1000));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
private calculateRetry503DelayMs(retryCount: number): number {
|
|
709
|
+
return Math.min(
|
|
710
|
+
DEFAULT_INGEST_RETRY_503_MAX_DELAY_MS,
|
|
711
|
+
DEFAULT_INGEST_RETRY_503_BASE_DELAY_MS * 2 ** retryCount
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
private async discardResponseBody(response: Response): Promise<void> {
|
|
716
|
+
if (response.bodyUsed || !response.body) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
try {
|
|
721
|
+
await response.arrayBuffer();
|
|
722
|
+
} catch {
|
|
723
|
+
try {
|
|
724
|
+
await response.body.cancel();
|
|
725
|
+
} catch {
|
|
726
|
+
// Best effort cleanup only; never mask retry/error flow.
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
private async sleep(delayMs: number, signal?: AbortSignal): Promise<void> {
|
|
732
|
+
if (delayMs <= 0) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
await new Promise<void>((resolve, reject) => {
|
|
737
|
+
const timer = setTimeout(() => {
|
|
738
|
+
cleanup();
|
|
739
|
+
resolve();
|
|
740
|
+
}, delayMs);
|
|
741
|
+
|
|
742
|
+
const onAbort = () => {
|
|
743
|
+
cleanup();
|
|
744
|
+
reject(signal?.reason ?? new DOMException("The operation was aborted.", "AbortError"));
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
const cleanup = () => {
|
|
748
|
+
clearTimeout(timer);
|
|
749
|
+
signal?.removeEventListener("abort", onAbort);
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
if (!signal) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (signal.aborted) {
|
|
757
|
+
onAbort();
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
578
765
|
private serializeEvent(
|
|
579
766
|
event: Record<string, unknown>
|
|
580
767
|
): Record<string, unknown> {
|
package/src/client/types.ts
CHANGED
|
@@ -152,6 +152,11 @@ export interface IngestOptions {
|
|
|
152
152
|
signal?: AbortSignal;
|
|
153
153
|
/** Wait for the ingestion to complete before returning */
|
|
154
154
|
wait?: boolean;
|
|
155
|
+
/**
|
|
156
|
+
* Number of retry attempts after the first request.
|
|
157
|
+
* Retries are disabled by default when undefined.
|
|
158
|
+
*/
|
|
159
|
+
maxRetries?: number;
|
|
155
160
|
}
|
|
156
161
|
|
|
157
162
|
/**
|