Skip to content

Commit 4f542ac

Browse files
committed
♻️ split uploader boundaries
Move the uploader implementation and its tests into a focused boundary-cleanup branch.
1 parent 4fd0a8a commit 4f542ac

8 files changed

Lines changed: 482 additions & 184 deletions

File tree

src/services/uploader.js

Lines changed: 0 additions & 101 deletions
This file was deleted.

src/uploader/core.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { basename } from 'node:path';
1414
export const DEFAULT_BATCH_SIZE = 50;
1515
export const DEFAULT_SHA_CHECK_BATCH_SIZE = 100;
1616
export const DEFAULT_TIMEOUT = 30000; // 30 seconds
17+
export const DEFAULT_BUILD_POLL_INTERVAL = 1000; // 1 second
1718

1819
// ============================================================================
1920
// Validation
@@ -340,17 +341,17 @@ export function resolveTimeout(options, uploadConfig) {
340341
* @param {number} timeout - Timeout in ms
341342
* @returns {boolean} True if timed out
342343
*/
343-
export function isTimedOut(startTime, timeout) {
344-
return Date.now() - startTime >= timeout;
344+
export function isTimedOut(startTime, timeout, now = Date.now()) {
345+
return now - startTime >= timeout;
345346
}
346347

347348
/**
348349
* Get elapsed time since start
349350
* @param {number} startTime - Start timestamp
350351
* @returns {number} Elapsed time in ms
351352
*/
352-
export function getElapsedTime(startTime) {
353-
return Date.now() - startTime;
353+
export function getElapsedTime(startTime, now = Date.now()) {
354+
return now - startTime;
354355
}
355356

356357
/**

src/uploader/index.js

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,91 @@
11
/**
22
* Uploader Module
33
*
4-
* Exports pure functions (core) and I/O operations for screenshot uploading.
4+
* Exports pure functions (core), I/O operations, and the public uploader
5+
* factory for screenshot uploading.
56
*/
67

8+
import { readFile, stat } from 'node:fs/promises';
9+
import { glob } from 'glob';
10+
import { checkShas, createApiClient, createBuild } from '../api/index.js';
11+
import {
12+
TimeoutError,
13+
UploadError,
14+
ValidationError,
15+
} from '../errors/vizzly-error.js';
16+
import { getDefaultBranch } from '../utils/git.js';
17+
import * as output from '../utils/output.js';
18+
import { resolveBatchSize, resolveTimeout } from './core.js';
19+
import {
20+
upload as uploadOperation,
21+
waitForBuild as waitForBuildOperation,
22+
} from './operations.js';
23+
24+
export function createUploader(
25+
{ apiKey, apiUrl, userAgent, command, upload: uploadConfig = {} } = {},
26+
options = {}
27+
) {
28+
let signal = options.signal || new AbortController().signal;
29+
let client = createApiClient({
30+
baseUrl: apiUrl,
31+
token: apiKey,
32+
command: command || 'upload',
33+
sdkUserAgent: userAgent,
34+
allowNoToken: true,
35+
});
36+
37+
let batchSize = resolveBatchSize(options, uploadConfig);
38+
let timeout = resolveTimeout(options, uploadConfig);
39+
let deps = options.deps || {
40+
client,
41+
createBuild,
42+
getDefaultBranch,
43+
glob,
44+
readFile,
45+
stat,
46+
checkShas,
47+
createError: (message, code, context) => {
48+
let error = new UploadError(message, context);
49+
error.code = code;
50+
return error;
51+
},
52+
createValidationError: (message, context) =>
53+
new ValidationError(message, context),
54+
createUploadError: (message, context) => new UploadError(message, context),
55+
createTimeoutError: (message, context) =>
56+
new TimeoutError(message, context),
57+
output,
58+
};
59+
60+
async function upload(uploadOptions) {
61+
return uploadOperation({
62+
uploadOptions,
63+
config: { apiKey, apiUrl },
64+
signal,
65+
batchSize,
66+
deps: {
67+
...deps,
68+
client: deps.client || client,
69+
},
70+
});
71+
}
72+
73+
async function waitForBuild(buildId, waitTimeout = timeout) {
74+
return waitForBuildOperation({
75+
buildId,
76+
timeout: waitTimeout,
77+
signal,
78+
client: deps.client || client,
79+
deps: {
80+
createError: deps.createError,
81+
createTimeoutError: deps.createTimeoutError,
82+
},
83+
});
84+
}
85+
86+
return { upload, waitForBuild };
87+
}
88+
789
// Core - pure functions
890
export {
991
buildBuildInfo,
@@ -18,6 +100,7 @@ export {
18100
buildWaitResult,
19101
computeSha256,
20102
DEFAULT_BATCH_SIZE,
103+
DEFAULT_BUILD_POLL_INTERVAL,
21104
DEFAULT_SHA_CHECK_BATCH_SIZE,
22105
DEFAULT_TIMEOUT,
23106
extractBrowserFromFilename,

src/uploader/operations.js

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
buildUploadingProgress,
1616
buildUploadResult,
1717
buildWaitResult,
18+
DEFAULT_BUILD_POLL_INTERVAL,
1819
DEFAULT_SHA_CHECK_BATCH_SIZE,
1920
extractStatusCodeFromError,
2021
fileToScreenshotFormat,
@@ -27,6 +28,38 @@ import {
2728
validateScreenshotsDir,
2829
} from './core.js';
2930

31+
function delay(ms, signal) {
32+
if (ms <= 0) {
33+
return Promise.resolve();
34+
}
35+
36+
return new Promise((resolve, reject) => {
37+
let timeoutId;
38+
39+
function cleanup() {
40+
clearTimeout(timeoutId);
41+
signal?.removeEventListener?.('abort', handleAbort);
42+
}
43+
44+
function handleAbort() {
45+
cleanup();
46+
reject(new Error('Operation cancelled'));
47+
}
48+
49+
timeoutId = setTimeout(() => {
50+
cleanup();
51+
resolve();
52+
}, ms);
53+
54+
if (signal?.aborted) {
55+
handleAbort();
56+
return;
57+
}
58+
59+
signal?.addEventListener?.('abort', handleAbort, { once: true });
60+
});
61+
}
62+
3063
// ============================================================================
3164
// File Discovery
3265
// ============================================================================
@@ -232,10 +265,16 @@ export async function uploadFiles({
232265
* @returns {Promise<Object>} Build result
233266
*/
234267
export async function waitForBuild({ buildId, timeout, signal, client, deps }) {
235-
let { createError, createTimeoutError } = deps;
236-
let startTime = Date.now();
268+
let {
269+
createError,
270+
createTimeoutError,
271+
now = () => Date.now(),
272+
pollInterval = DEFAULT_BUILD_POLL_INTERVAL,
273+
wait = delay,
274+
} = deps;
275+
let startTime = now();
237276

238-
while (!isTimedOut(startTime, timeout)) {
277+
while (!isTimedOut(startTime, timeout, now())) {
239278
if (signal.aborted) {
240279
throw createError('Operation cancelled', 'UPLOAD_CANCELLED', { buildId });
241280
}
@@ -263,12 +302,19 @@ export async function waitForBuild({ buildId, timeout, signal, client, deps }) {
263302
'BUILD_FAILED'
264303
);
265304
}
305+
306+
let remaining = timeout - getElapsedTime(startTime, now());
307+
if (remaining <= 0) {
308+
break;
309+
}
310+
311+
await wait(Math.min(pollInterval, remaining), signal);
266312
}
267313

268314
throw createTimeoutError(`Build timed out after ${timeout}ms`, {
269315
buildId,
270316
timeout,
271-
elapsed: getElapsedTime(startTime),
317+
elapsed: getElapsedTime(startTime, now()),
272318
});
273319
}
274320

tests/services/uploader.test.js

Lines changed: 0 additions & 74 deletions
This file was deleted.

0 commit comments

Comments
 (0)