flowforge-client 0.1.4 → 0.2.1
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/index.d.mts +28 -2
- package/dist/index.d.ts +28 -2
- package/dist/index.js +101 -2
- package/dist/index.mjs +101 -2
- package/package.json +8 -3
package/dist/index.d.mts
CHANGED
|
@@ -269,6 +269,27 @@ interface ClientOptions {
|
|
|
269
269
|
apiKey?: string;
|
|
270
270
|
/** Custom fetch implementation (for testing or custom environments) */
|
|
271
271
|
fetch?: typeof fetch;
|
|
272
|
+
/**
|
|
273
|
+
* Request timeout in milliseconds.
|
|
274
|
+
* Requests will be aborted if they exceed this duration.
|
|
275
|
+
* Default: 30000 (30 seconds)
|
|
276
|
+
*/
|
|
277
|
+
timeout?: number;
|
|
278
|
+
/**
|
|
279
|
+
* Retry configuration for failed requests.
|
|
280
|
+
*/
|
|
281
|
+
retry?: {
|
|
282
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
283
|
+
maxAttempts?: number;
|
|
284
|
+
/** Base delay in milliseconds for exponential backoff (default: 1000) */
|
|
285
|
+
baseDelay?: number;
|
|
286
|
+
/** Maximum delay in milliseconds (default: 30000) */
|
|
287
|
+
maxDelay?: number;
|
|
288
|
+
/** Whether to retry on timeout errors (default: true) */
|
|
289
|
+
retryOnTimeout?: boolean;
|
|
290
|
+
/** HTTP status codes that should trigger a retry (default: [429, 500, 502, 503, 504]) */
|
|
291
|
+
retryableStatuses?: number[];
|
|
292
|
+
};
|
|
272
293
|
}
|
|
273
294
|
|
|
274
295
|
/**
|
|
@@ -896,10 +917,15 @@ interface FlowForgeClient {
|
|
|
896
917
|
* });
|
|
897
918
|
* ```
|
|
898
919
|
*
|
|
899
|
-
* @example With API key
|
|
920
|
+
* @example With API key and timeout
|
|
900
921
|
* ```ts
|
|
901
922
|
* const ff = createClient('http://localhost:8000', {
|
|
902
|
-
* apiKey: process.env.FLOWFORGE_API_KEY
|
|
923
|
+
* apiKey: process.env.FLOWFORGE_API_KEY,
|
|
924
|
+
* timeout: 60000, // 60 seconds
|
|
925
|
+
* retry: {
|
|
926
|
+
* maxAttempts: 5,
|
|
927
|
+
* baseDelay: 2000,
|
|
928
|
+
* }
|
|
903
929
|
* });
|
|
904
930
|
* ```
|
|
905
931
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -269,6 +269,27 @@ interface ClientOptions {
|
|
|
269
269
|
apiKey?: string;
|
|
270
270
|
/** Custom fetch implementation (for testing or custom environments) */
|
|
271
271
|
fetch?: typeof fetch;
|
|
272
|
+
/**
|
|
273
|
+
* Request timeout in milliseconds.
|
|
274
|
+
* Requests will be aborted if they exceed this duration.
|
|
275
|
+
* Default: 30000 (30 seconds)
|
|
276
|
+
*/
|
|
277
|
+
timeout?: number;
|
|
278
|
+
/**
|
|
279
|
+
* Retry configuration for failed requests.
|
|
280
|
+
*/
|
|
281
|
+
retry?: {
|
|
282
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
283
|
+
maxAttempts?: number;
|
|
284
|
+
/** Base delay in milliseconds for exponential backoff (default: 1000) */
|
|
285
|
+
baseDelay?: number;
|
|
286
|
+
/** Maximum delay in milliseconds (default: 30000) */
|
|
287
|
+
maxDelay?: number;
|
|
288
|
+
/** Whether to retry on timeout errors (default: true) */
|
|
289
|
+
retryOnTimeout?: boolean;
|
|
290
|
+
/** HTTP status codes that should trigger a retry (default: [429, 500, 502, 503, 504]) */
|
|
291
|
+
retryableStatuses?: number[];
|
|
292
|
+
};
|
|
272
293
|
}
|
|
273
294
|
|
|
274
295
|
/**
|
|
@@ -896,10 +917,15 @@ interface FlowForgeClient {
|
|
|
896
917
|
* });
|
|
897
918
|
* ```
|
|
898
919
|
*
|
|
899
|
-
* @example With API key
|
|
920
|
+
* @example With API key and timeout
|
|
900
921
|
* ```ts
|
|
901
922
|
* const ff = createClient('http://localhost:8000', {
|
|
902
|
-
* apiKey: process.env.FLOWFORGE_API_KEY
|
|
923
|
+
* apiKey: process.env.FLOWFORGE_API_KEY,
|
|
924
|
+
* timeout: 60000, // 60 seconds
|
|
925
|
+
* retry: {
|
|
926
|
+
* maxAttempts: 5,
|
|
927
|
+
* baseDelay: 2000,
|
|
928
|
+
* }
|
|
903
929
|
* });
|
|
904
930
|
* ```
|
|
905
931
|
*/
|
package/dist/index.js
CHANGED
|
@@ -733,22 +733,60 @@ var ApiKeysResource = class {
|
|
|
733
733
|
};
|
|
734
734
|
|
|
735
735
|
// src/client.ts
|
|
736
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
737
|
+
function anySignal(signals) {
|
|
738
|
+
const controller = new AbortController();
|
|
739
|
+
for (const signal of signals) {
|
|
740
|
+
if (signal.aborted) {
|
|
741
|
+
controller.abort(signal.reason);
|
|
742
|
+
return controller.signal;
|
|
743
|
+
}
|
|
744
|
+
signal.addEventListener(
|
|
745
|
+
"abort",
|
|
746
|
+
() => controller.abort(signal.reason),
|
|
747
|
+
{ once: true }
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
return controller.signal;
|
|
751
|
+
}
|
|
752
|
+
var DEFAULT_RETRY = {
|
|
753
|
+
maxAttempts: 3,
|
|
754
|
+
baseDelay: 1e3,
|
|
755
|
+
maxDelay: 3e4,
|
|
756
|
+
retryOnTimeout: true,
|
|
757
|
+
retryableStatuses: [429, 500, 502, 503, 504]
|
|
758
|
+
};
|
|
759
|
+
function calculateBackoff(attempt, baseDelay, maxDelay) {
|
|
760
|
+
const exponentialDelay = baseDelay * Math.pow(2, attempt);
|
|
761
|
+
const jitter = 0.5 + Math.random();
|
|
762
|
+
return Math.min(exponentialDelay * jitter, maxDelay);
|
|
763
|
+
}
|
|
764
|
+
function sleep(ms) {
|
|
765
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
766
|
+
}
|
|
736
767
|
function createClient(baseUrl, options = {}) {
|
|
737
768
|
const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
|
|
738
769
|
const fetchFn = options.fetch || globalThis.fetch;
|
|
739
|
-
|
|
770
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
771
|
+
const retryConfig = { ...DEFAULT_RETRY, ...options.retry };
|
|
772
|
+
async function executeRequest(method, path, body, signal) {
|
|
740
773
|
const headers = {
|
|
741
774
|
"Content-Type": "application/json"
|
|
742
775
|
};
|
|
743
776
|
if (options.apiKey) {
|
|
744
777
|
headers["X-FlowForge-API-Key"] = options.apiKey;
|
|
745
778
|
}
|
|
779
|
+
const timeoutController = new AbortController();
|
|
780
|
+
const timeoutId = setTimeout(() => timeoutController.abort(), timeout);
|
|
781
|
+
const combinedSignal = signal ? anySignal([signal, timeoutController.signal]) : timeoutController.signal;
|
|
746
782
|
try {
|
|
747
783
|
const response = await fetchFn(`${normalizedBaseUrl}/api/v1${path}`, {
|
|
748
784
|
method,
|
|
749
785
|
headers,
|
|
750
|
-
body: body ? JSON.stringify(body) : void 0
|
|
786
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
787
|
+
signal: combinedSignal
|
|
751
788
|
});
|
|
789
|
+
clearTimeout(timeoutId);
|
|
752
790
|
if (!response.ok) {
|
|
753
791
|
const errorBody = await response.json().catch(() => ({}));
|
|
754
792
|
const error = {
|
|
@@ -763,6 +801,27 @@ function createClient(baseUrl, options = {}) {
|
|
|
763
801
|
const data = await response.json();
|
|
764
802
|
return { data, error: null };
|
|
765
803
|
} catch (err) {
|
|
804
|
+
clearTimeout(timeoutId);
|
|
805
|
+
if (timeoutController.signal.aborted) {
|
|
806
|
+
const error2 = {
|
|
807
|
+
message: `Request timed out after ${timeout}ms`,
|
|
808
|
+
status: 0,
|
|
809
|
+
code: "TIMEOUT",
|
|
810
|
+
detail: { timeout },
|
|
811
|
+
name: "FlowForgeError"
|
|
812
|
+
};
|
|
813
|
+
return { data: null, error: error2 };
|
|
814
|
+
}
|
|
815
|
+
if (signal?.aborted) {
|
|
816
|
+
const error2 = {
|
|
817
|
+
message: "Request was cancelled",
|
|
818
|
+
status: 0,
|
|
819
|
+
code: "CANCELLED",
|
|
820
|
+
detail: err,
|
|
821
|
+
name: "FlowForgeError"
|
|
822
|
+
};
|
|
823
|
+
return { data: null, error: error2 };
|
|
824
|
+
}
|
|
766
825
|
const error = {
|
|
767
826
|
message: err instanceof Error ? err.message : "Network error",
|
|
768
827
|
status: 0,
|
|
@@ -773,6 +832,46 @@ function createClient(baseUrl, options = {}) {
|
|
|
773
832
|
return { data: null, error };
|
|
774
833
|
}
|
|
775
834
|
}
|
|
835
|
+
function isRetryable(error) {
|
|
836
|
+
if (error.code === "TIMEOUT" && retryConfig.retryOnTimeout) {
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
if (error.code === "NETWORK_ERROR") {
|
|
840
|
+
return true;
|
|
841
|
+
}
|
|
842
|
+
if (retryConfig.retryableStatuses.includes(error.status)) {
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
async function request(method, path, body) {
|
|
848
|
+
let lastError = null;
|
|
849
|
+
for (let attempt = 0; attempt < retryConfig.maxAttempts; attempt++) {
|
|
850
|
+
const result = await executeRequest(method, path, body);
|
|
851
|
+
if (result.data !== null) {
|
|
852
|
+
return result;
|
|
853
|
+
}
|
|
854
|
+
lastError = result.error;
|
|
855
|
+
if (!lastError || !isRetryable(lastError) || attempt === retryConfig.maxAttempts - 1) {
|
|
856
|
+
return result;
|
|
857
|
+
}
|
|
858
|
+
const delay = calculateBackoff(
|
|
859
|
+
attempt,
|
|
860
|
+
retryConfig.baseDelay,
|
|
861
|
+
retryConfig.maxDelay
|
|
862
|
+
);
|
|
863
|
+
await sleep(delay);
|
|
864
|
+
}
|
|
865
|
+
return {
|
|
866
|
+
data: null,
|
|
867
|
+
error: lastError || {
|
|
868
|
+
message: "Unknown error",
|
|
869
|
+
status: 0,
|
|
870
|
+
code: "UNKNOWN",
|
|
871
|
+
name: "FlowForgeError"
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
}
|
|
776
875
|
return {
|
|
777
876
|
events: new EventsResource(request),
|
|
778
877
|
runs: new RunsResource(request),
|
package/dist/index.mjs
CHANGED
|
@@ -697,22 +697,60 @@ var ApiKeysResource = class {
|
|
|
697
697
|
};
|
|
698
698
|
|
|
699
699
|
// src/client.ts
|
|
700
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
701
|
+
function anySignal(signals) {
|
|
702
|
+
const controller = new AbortController();
|
|
703
|
+
for (const signal of signals) {
|
|
704
|
+
if (signal.aborted) {
|
|
705
|
+
controller.abort(signal.reason);
|
|
706
|
+
return controller.signal;
|
|
707
|
+
}
|
|
708
|
+
signal.addEventListener(
|
|
709
|
+
"abort",
|
|
710
|
+
() => controller.abort(signal.reason),
|
|
711
|
+
{ once: true }
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
return controller.signal;
|
|
715
|
+
}
|
|
716
|
+
var DEFAULT_RETRY = {
|
|
717
|
+
maxAttempts: 3,
|
|
718
|
+
baseDelay: 1e3,
|
|
719
|
+
maxDelay: 3e4,
|
|
720
|
+
retryOnTimeout: true,
|
|
721
|
+
retryableStatuses: [429, 500, 502, 503, 504]
|
|
722
|
+
};
|
|
723
|
+
function calculateBackoff(attempt, baseDelay, maxDelay) {
|
|
724
|
+
const exponentialDelay = baseDelay * Math.pow(2, attempt);
|
|
725
|
+
const jitter = 0.5 + Math.random();
|
|
726
|
+
return Math.min(exponentialDelay * jitter, maxDelay);
|
|
727
|
+
}
|
|
728
|
+
function sleep(ms) {
|
|
729
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
730
|
+
}
|
|
700
731
|
function createClient(baseUrl, options = {}) {
|
|
701
732
|
const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
|
|
702
733
|
const fetchFn = options.fetch || globalThis.fetch;
|
|
703
|
-
|
|
734
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
735
|
+
const retryConfig = { ...DEFAULT_RETRY, ...options.retry };
|
|
736
|
+
async function executeRequest(method, path, body, signal) {
|
|
704
737
|
const headers = {
|
|
705
738
|
"Content-Type": "application/json"
|
|
706
739
|
};
|
|
707
740
|
if (options.apiKey) {
|
|
708
741
|
headers["X-FlowForge-API-Key"] = options.apiKey;
|
|
709
742
|
}
|
|
743
|
+
const timeoutController = new AbortController();
|
|
744
|
+
const timeoutId = setTimeout(() => timeoutController.abort(), timeout);
|
|
745
|
+
const combinedSignal = signal ? anySignal([signal, timeoutController.signal]) : timeoutController.signal;
|
|
710
746
|
try {
|
|
711
747
|
const response = await fetchFn(`${normalizedBaseUrl}/api/v1${path}`, {
|
|
712
748
|
method,
|
|
713
749
|
headers,
|
|
714
|
-
body: body ? JSON.stringify(body) : void 0
|
|
750
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
751
|
+
signal: combinedSignal
|
|
715
752
|
});
|
|
753
|
+
clearTimeout(timeoutId);
|
|
716
754
|
if (!response.ok) {
|
|
717
755
|
const errorBody = await response.json().catch(() => ({}));
|
|
718
756
|
const error = {
|
|
@@ -727,6 +765,27 @@ function createClient(baseUrl, options = {}) {
|
|
|
727
765
|
const data = await response.json();
|
|
728
766
|
return { data, error: null };
|
|
729
767
|
} catch (err) {
|
|
768
|
+
clearTimeout(timeoutId);
|
|
769
|
+
if (timeoutController.signal.aborted) {
|
|
770
|
+
const error2 = {
|
|
771
|
+
message: `Request timed out after ${timeout}ms`,
|
|
772
|
+
status: 0,
|
|
773
|
+
code: "TIMEOUT",
|
|
774
|
+
detail: { timeout },
|
|
775
|
+
name: "FlowForgeError"
|
|
776
|
+
};
|
|
777
|
+
return { data: null, error: error2 };
|
|
778
|
+
}
|
|
779
|
+
if (signal?.aborted) {
|
|
780
|
+
const error2 = {
|
|
781
|
+
message: "Request was cancelled",
|
|
782
|
+
status: 0,
|
|
783
|
+
code: "CANCELLED",
|
|
784
|
+
detail: err,
|
|
785
|
+
name: "FlowForgeError"
|
|
786
|
+
};
|
|
787
|
+
return { data: null, error: error2 };
|
|
788
|
+
}
|
|
730
789
|
const error = {
|
|
731
790
|
message: err instanceof Error ? err.message : "Network error",
|
|
732
791
|
status: 0,
|
|
@@ -737,6 +796,46 @@ function createClient(baseUrl, options = {}) {
|
|
|
737
796
|
return { data: null, error };
|
|
738
797
|
}
|
|
739
798
|
}
|
|
799
|
+
function isRetryable(error) {
|
|
800
|
+
if (error.code === "TIMEOUT" && retryConfig.retryOnTimeout) {
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
if (error.code === "NETWORK_ERROR") {
|
|
804
|
+
return true;
|
|
805
|
+
}
|
|
806
|
+
if (retryConfig.retryableStatuses.includes(error.status)) {
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
async function request(method, path, body) {
|
|
812
|
+
let lastError = null;
|
|
813
|
+
for (let attempt = 0; attempt < retryConfig.maxAttempts; attempt++) {
|
|
814
|
+
const result = await executeRequest(method, path, body);
|
|
815
|
+
if (result.data !== null) {
|
|
816
|
+
return result;
|
|
817
|
+
}
|
|
818
|
+
lastError = result.error;
|
|
819
|
+
if (!lastError || !isRetryable(lastError) || attempt === retryConfig.maxAttempts - 1) {
|
|
820
|
+
return result;
|
|
821
|
+
}
|
|
822
|
+
const delay = calculateBackoff(
|
|
823
|
+
attempt,
|
|
824
|
+
retryConfig.baseDelay,
|
|
825
|
+
retryConfig.maxDelay
|
|
826
|
+
);
|
|
827
|
+
await sleep(delay);
|
|
828
|
+
}
|
|
829
|
+
return {
|
|
830
|
+
data: null,
|
|
831
|
+
error: lastError || {
|
|
832
|
+
message: "Unknown error",
|
|
833
|
+
status: 0,
|
|
834
|
+
code: "UNKNOWN",
|
|
835
|
+
name: "FlowForgeError"
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
}
|
|
740
839
|
return {
|
|
741
840
|
events: new EventsResource(request),
|
|
742
841
|
runs: new RunsResource(request),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowforge-client",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "TypeScript client for FlowForge workflow orchestration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -24,12 +24,17 @@
|
|
|
24
24
|
],
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"devDependencies": {
|
|
27
|
+
"@vitest/coverage-v8": "^2.0.0",
|
|
27
28
|
"tsup": "^8.0.0",
|
|
28
|
-
"typescript": "^5.3.0"
|
|
29
|
+
"typescript": "^5.3.0",
|
|
30
|
+
"vitest": "^2.0.0"
|
|
29
31
|
},
|
|
30
32
|
"scripts": {
|
|
31
33
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
32
34
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
33
|
-
"typecheck": "tsc --noEmit"
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest",
|
|
38
|
+
"test:coverage": "vitest run --coverage"
|
|
34
39
|
}
|
|
35
40
|
}
|