@pixeyedev/core 0.4.3 → 0.4.4
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/client.js +17 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/retry.d.ts +14 -0
- package/dist/retry.js +40 -0
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CreateBuildResponseSchema, ListBuildsResponseSchema, PipelineFinalizeResponseSchema, PresignUploadResponseSchema, SubmitShardResponseSchema } from '@pixeyedev/shared';
|
|
2
|
+
import { isRetryableHttpError, maskPresignedUrl, withRetry } from './retry.js';
|
|
2
3
|
export class PixeyeError extends Error {
|
|
3
4
|
status;
|
|
4
5
|
path;
|
|
@@ -58,14 +59,23 @@ export class PixeyeClient {
|
|
|
58
59
|
return PresignUploadResponseSchema.parse(raw);
|
|
59
60
|
}
|
|
60
61
|
async uploadFile(presignedUrl, buffer, contentType) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
const maskedUrl = maskPresignedUrl(presignedUrl);
|
|
63
|
+
await withRetry(async () => {
|
|
64
|
+
const res = await fetch(presignedUrl, {
|
|
65
|
+
method: 'PUT',
|
|
66
|
+
body: new Uint8Array(buffer),
|
|
67
|
+
headers: { 'Content-Type': contentType }
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
throw new PixeyeError(res.status, maskedUrl, await res.text());
|
|
71
|
+
}
|
|
72
|
+
}, isRetryableHttpError, {
|
|
73
|
+
maxAttempts: 3,
|
|
74
|
+
baseDelayMs: 1000,
|
|
75
|
+
onRetry: (attempt, err) => {
|
|
76
|
+
console.warn(`[pixeye] Upload retry ${attempt}/3: ${err.message}`);
|
|
77
|
+
}
|
|
65
78
|
});
|
|
66
|
-
if (!res.ok) {
|
|
67
|
-
throw new PixeyeError(res.status, presignedUrl, await res.text());
|
|
68
|
-
}
|
|
69
79
|
}
|
|
70
80
|
async submitShard(opts) {
|
|
71
81
|
const raw = await this.request(`/api/v1/builds/${opts.buildId}/shards`, {
|
package/dist/index.d.ts
CHANGED
|
@@ -7,4 +7,6 @@ export { detectBaseBranchFromGit, detectCIEnvironment } from './ci.js';
|
|
|
7
7
|
export type { DiscoveredFile } from './discover.js';
|
|
8
8
|
export { discoverScreenshots } from './discover.js';
|
|
9
9
|
export { runParallel } from './parallel.js';
|
|
10
|
+
export type { RetryOptions } from './retry.js';
|
|
11
|
+
export { isRetryableHttpError, maskPresignedUrl, withRetry } from './retry.js';
|
|
10
12
|
export { DEFAULT_SERVER_URL, DEFAULT_UPLOAD_CONCURRENCY } from './constants.js';
|
package/dist/index.js
CHANGED
|
@@ -4,5 +4,6 @@ export { detectBaseBranchFromGit, detectCIEnvironment } from './ci.js';
|
|
|
4
4
|
export { discoverScreenshots } from './discover.js';
|
|
5
5
|
// Parallel execution
|
|
6
6
|
export { runParallel } from './parallel.js';
|
|
7
|
+
export { isRetryableHttpError, maskPresignedUrl, withRetry } from './retry.js';
|
|
7
8
|
// Constants
|
|
8
9
|
export { DEFAULT_SERVER_URL, DEFAULT_UPLOAD_CONCURRENCY } from './constants.js';
|
package/dist/retry.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface RetryOptions {
|
|
2
|
+
maxAttempts?: number;
|
|
3
|
+
baseDelayMs?: number;
|
|
4
|
+
onRetry?: (attempt: number, error: Error) => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Retry an async function with exponential backoff + jitter.
|
|
8
|
+
* Only retries on errors where shouldRetry returns true.
|
|
9
|
+
*/
|
|
10
|
+
export declare const withRetry: <T>(fn: () => Promise<T>, shouldRetry: (error: unknown) => boolean, opts?: RetryOptions) => Promise<T>;
|
|
11
|
+
/** Returns true for HTTP 429/5xx responses or transient network errors. */
|
|
12
|
+
export declare const isRetryableHttpError: (error: unknown) => boolean;
|
|
13
|
+
/** Mask presigned URL signature for safe error logging. */
|
|
14
|
+
export declare const maskPresignedUrl: (url: string) => string;
|
package/dist/retry.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry an async function with exponential backoff + jitter.
|
|
3
|
+
* Only retries on errors where shouldRetry returns true.
|
|
4
|
+
*/
|
|
5
|
+
export const withRetry = async (fn, shouldRetry, opts = {}) => {
|
|
6
|
+
const maxAttempts = opts.maxAttempts ?? 3;
|
|
7
|
+
const baseDelayMs = opts.baseDelayMs ?? 1000;
|
|
8
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
9
|
+
try {
|
|
10
|
+
return await fn();
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (attempt === maxAttempts || !shouldRetry(error)) {
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
// Exponential backoff with equal jitter (half deterministic + half random)
|
|
17
|
+
const exponential = baseDelayMs * Math.pow(2, attempt - 1);
|
|
18
|
+
const delay = exponential * (0.5 + 0.5 * Math.random());
|
|
19
|
+
opts.onRetry?.(attempt, error instanceof Error ? error : new Error(String(error)));
|
|
20
|
+
await new Promise(r => setTimeout(r, delay));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
throw new Error('Unreachable');
|
|
24
|
+
};
|
|
25
|
+
const TRANSIENT_NETWORK_RE = /fetch failed|ECONNRESET|ETIMEDOUT|ECONNREFUSED|ENOTFOUND|EPIPE|socket hang up/i;
|
|
26
|
+
/** Returns true for HTTP 429/5xx responses or transient network errors. */
|
|
27
|
+
export const isRetryableHttpError = (error) => {
|
|
28
|
+
// Check status code first (authoritative when present)
|
|
29
|
+
const status = error?.status;
|
|
30
|
+
if (status !== undefined) {
|
|
31
|
+
return status === 429 || (status >= 500 && status < 600);
|
|
32
|
+
}
|
|
33
|
+
// Fall back to message-based detection for genuine network errors (no HTTP response)
|
|
34
|
+
if (error instanceof Error && TRANSIENT_NETWORK_RE.test(error.message)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
};
|
|
39
|
+
/** Mask presigned URL signature for safe error logging. */
|
|
40
|
+
export const maskPresignedUrl = (url) => url.split('?')[0] + '?[signature-masked]';
|