@devicecloud.dev/dcd 5.7.1 → 5.8.0-beta.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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TAppMetadata } from '../types.js';
|
|
2
2
|
import type { AuthContext } from '../types/domain/auth.types.js';
|
|
3
3
|
import type { LiveCommandStatus, LiveExecResult, LiveSession, LiveSessionSummary } from '../types/domain/live.types.js';
|
|
4
|
-
import { components } from '../types/generated/schema.types.js';
|
|
4
|
+
import { components, paths } from '../types/generated/schema.types.js';
|
|
5
5
|
/**
|
|
6
6
|
* Error thrown for non-OK API responses, carrying the HTTP status so callers
|
|
7
7
|
* can branch on auth failures etc. without string matching.
|
|
@@ -10,6 +10,17 @@ export declare class ApiError extends Error {
|
|
|
10
10
|
status: number;
|
|
11
11
|
constructor(message: string, status: number);
|
|
12
12
|
}
|
|
13
|
+
type ResultsResponse = paths['/results/{uploadId}']['get']['responses']['200']['content']['application/json'];
|
|
14
|
+
/**
|
|
15
|
+
* The last results body a poll loop received and the ETag it came with, so
|
|
16
|
+
* the next poll can revalidate with `If-None-Match` and get an empty 304 back
|
|
17
|
+
* when nothing changed. Create one per poll loop; `getResultsForUpload` fills
|
|
18
|
+
* and clears it.
|
|
19
|
+
*/
|
|
20
|
+
export interface ResultsPollCache {
|
|
21
|
+
body?: ResultsResponse;
|
|
22
|
+
etag?: string;
|
|
23
|
+
}
|
|
13
24
|
export declare const ApiGateway: {
|
|
14
25
|
/**
|
|
15
26
|
* Enhances generic "fetch failed" errors with more specific diagnostic information
|
|
@@ -98,7 +109,23 @@ export declare const ApiGateway: {
|
|
|
98
109
|
token?: string;
|
|
99
110
|
}>;
|
|
100
111
|
finishLargeFile(baseUrl: string, auth: AuthContext, fileId: string, partSha1Array: string[]): Promise<unknown>;
|
|
101
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Fetch the result rows for an upload, for the `dcd cloud` poll loop.
|
|
114
|
+
*
|
|
115
|
+
* Asks for `?view=summary`, which leaves out `result_files` and the rest of
|
|
116
|
+
* the columns the CLI never reads. An API that predates the view ignores
|
|
117
|
+
* the parameter and returns the full rows, a superset, so this works
|
|
118
|
+
* against either.
|
|
119
|
+
*
|
|
120
|
+
* With a `cache`, a repeat poll revalidates with `If-None-Match` and a 304
|
|
121
|
+
* hands back the cached body.
|
|
122
|
+
* @param baseUrl API base URL
|
|
123
|
+
* @param auth AuthContext carrying request headers
|
|
124
|
+
* @param uploadId Upload ID to fetch results for
|
|
125
|
+
* @param cache Optional per-poll-loop cache, filled and cleared here
|
|
126
|
+
* @returns The results response body
|
|
127
|
+
*/
|
|
128
|
+
getResultsForUpload(baseUrl: string, auth: AuthContext, uploadId: string, cache?: ResultsPollCache): Promise<{
|
|
102
129
|
statusCode?: number;
|
|
103
130
|
results?: components["schemas"]["TResultResponse"][];
|
|
104
131
|
}>;
|
|
@@ -209,3 +236,4 @@ export declare const ApiGateway: {
|
|
|
209
236
|
stopLiveSession(baseUrl: string, auth: AuthContext, sessionName: string): Promise<void>;
|
|
210
237
|
getLiveSession(baseUrl: string, auth: AuthContext, sessionName: string): Promise<LiveSession>;
|
|
211
238
|
};
|
|
239
|
+
export {};
|
|
@@ -347,20 +347,73 @@ export const ApiGateway = {
|
|
|
347
347
|
throw error;
|
|
348
348
|
}
|
|
349
349
|
},
|
|
350
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Fetch the result rows for an upload, for the `dcd cloud` poll loop.
|
|
352
|
+
*
|
|
353
|
+
* Asks for `?view=summary`, which leaves out `result_files` and the rest of
|
|
354
|
+
* the columns the CLI never reads. An API that predates the view ignores
|
|
355
|
+
* the parameter and returns the full rows, a superset, so this works
|
|
356
|
+
* against either.
|
|
357
|
+
*
|
|
358
|
+
* With a `cache`, a repeat poll revalidates with `If-None-Match` and a 304
|
|
359
|
+
* hands back the cached body.
|
|
360
|
+
* @param baseUrl API base URL
|
|
361
|
+
* @param auth AuthContext carrying request headers
|
|
362
|
+
* @param uploadId Upload ID to fetch results for
|
|
363
|
+
* @param cache Optional per-poll-loop cache, filled and cleared here
|
|
364
|
+
* @returns The results response body
|
|
365
|
+
*/
|
|
366
|
+
async getResultsForUpload(baseUrl, auth, uploadId, cache) {
|
|
351
367
|
// TODO: merge with getUploadStatus
|
|
368
|
+
const url = `${baseUrl}/results/${uploadId}?view=summary`;
|
|
352
369
|
try {
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
370
|
+
const headers = { ...auth.headers };
|
|
371
|
+
if (cache?.body && cache.etag) {
|
|
372
|
+
headers['if-none-match'] = cache.etag;
|
|
373
|
+
// Per the Fetch spec, undici appends `Cache-Control: no-cache` to any
|
|
374
|
+
// request that carries a conditional header and no Cache-Control of
|
|
375
|
+
// its own, and Express's `fresh` never answers 304 to a no-cache
|
|
376
|
+
// request. So every poll would get a full 200. max-age=0 still asks
|
|
377
|
+
// for revalidation, and keeps undici from adding no-cache.
|
|
378
|
+
headers['cache-control'] = 'max-age=0';
|
|
379
|
+
}
|
|
380
|
+
const res = await fetch(url, { headers });
|
|
381
|
+
// Before the `!res.ok` check, since a 304 is not `ok`.
|
|
382
|
+
if (res.status === 304) {
|
|
383
|
+
if (cache?.body) {
|
|
384
|
+
return cache.body;
|
|
385
|
+
}
|
|
386
|
+
// We sent no If-None-Match, so there is nothing to reuse. Start over
|
|
387
|
+
// with an unconditional request on the next poll.
|
|
388
|
+
if (cache) {
|
|
389
|
+
delete cache.body;
|
|
390
|
+
delete cache.etag;
|
|
391
|
+
}
|
|
392
|
+
throw new Error('Failed to get results: API returned 304 Not Modified to an unconditional request');
|
|
393
|
+
}
|
|
356
394
|
if (!res.ok) {
|
|
357
395
|
await this.handleApiError(res, 'Failed to get results');
|
|
358
396
|
}
|
|
359
|
-
|
|
397
|
+
const body = await parseJsonResponse(res, 'Failed to get results');
|
|
398
|
+
if (cache) {
|
|
399
|
+
// Only pin a real result set. The API reports some errors as a 200
|
|
400
|
+
// with a `{ statusCode, message }` envelope, and an empty set is
|
|
401
|
+
// rejected by the poll loop; caching either would replay it on 304.
|
|
402
|
+
const etag = res.headers.get('etag');
|
|
403
|
+
if (etag && Array.isArray(body.results) && body.results.length > 0) {
|
|
404
|
+
cache.body = body;
|
|
405
|
+
cache.etag = etag;
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
delete cache.body;
|
|
409
|
+
delete cache.etag;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return body;
|
|
360
413
|
}
|
|
361
414
|
catch (error) {
|
|
362
415
|
if (error instanceof TypeError && error.message === 'fetch failed') {
|
|
363
|
-
throw this.enhanceFetchError(error,
|
|
416
|
+
throw this.enhanceFetchError(error, url);
|
|
364
417
|
}
|
|
365
418
|
throw error;
|
|
366
419
|
}
|
|
@@ -118,6 +118,7 @@ export declare class ResultsPollingService {
|
|
|
118
118
|
* @param apiUrl API base URL
|
|
119
119
|
* @param auth AuthContext carrying request headers
|
|
120
120
|
* @param uploadId Upload ID to fetch results for
|
|
121
|
+
* @param cache The poll loop's results cache, for ETag revalidation
|
|
121
122
|
* @param debug Whether debug logging is enabled
|
|
122
123
|
* @param logger Optional logger function
|
|
123
124
|
* @returns Promise resolving to test results
|
|
@@ -133,6 +133,9 @@ export class ResultsPollingService {
|
|
|
133
133
|
let { auth } = options;
|
|
134
134
|
this.initializePollingDisplay(json, logger);
|
|
135
135
|
let sequentialPollFailures = 0;
|
|
136
|
+
// The last result set and its ETag, so a poll that finds nothing changed
|
|
137
|
+
// gets an empty 304 instead of the whole set again.
|
|
138
|
+
const resultsCache = {};
|
|
136
139
|
const pollIntervalMs = auth.mode === 'bearer'
|
|
137
140
|
? this.BEARER_POLL_INTERVAL_MS
|
|
138
141
|
: this.APIKEY_POLL_INTERVAL_MS;
|
|
@@ -229,7 +232,7 @@ export class ResultsPollingService {
|
|
|
229
232
|
nextPollAt = null;
|
|
230
233
|
renderStatus();
|
|
231
234
|
auth = await this.refreshPollingAuth(auth, uploadId, subscription, debug, logger);
|
|
232
|
-
const updatedResults = await this.fetchAndLogResults(apiUrl, auth, uploadId, debug, logger);
|
|
235
|
+
const updatedResults = await this.fetchAndLogResults(apiUrl, auth, uploadId, resultsCache, debug, logger);
|
|
233
236
|
const { summary } = this.calculateStatusSummary(updatedResults);
|
|
234
237
|
if (!json) {
|
|
235
238
|
statusBody = this.buildStatusBody(updatedResults, quiet, summary);
|
|
@@ -395,21 +398,28 @@ export class ResultsPollingService {
|
|
|
395
398
|
* @param apiUrl API base URL
|
|
396
399
|
* @param auth AuthContext carrying request headers
|
|
397
400
|
* @param uploadId Upload ID to fetch results for
|
|
401
|
+
* @param cache The poll loop's results cache, for ETag revalidation
|
|
398
402
|
* @param debug Whether debug logging is enabled
|
|
399
403
|
* @param logger Optional logger function
|
|
400
404
|
* @returns Promise resolving to test results
|
|
401
405
|
*/
|
|
402
|
-
async fetchAndLogResults(apiUrl, auth, uploadId, debug, logger) {
|
|
406
|
+
async fetchAndLogResults(apiUrl, auth, uploadId, cache, debug, logger) {
|
|
403
407
|
if (debug && logger) {
|
|
404
408
|
logger(`[DEBUG] Polling for results: ${uploadId}`);
|
|
405
409
|
}
|
|
406
|
-
|
|
410
|
+
// A 304 hands back the cached body itself; a 200 is always a new object.
|
|
411
|
+
const cachedBody = cache.body;
|
|
412
|
+
const response = await ApiGateway.getResultsForUpload(apiUrl, auth, uploadId, cache);
|
|
413
|
+
const notModified = cachedBody !== undefined && response === cachedBody;
|
|
414
|
+
const { results: updatedResults } = response;
|
|
407
415
|
// An empty array would otherwise read as "all complete, all passed".
|
|
408
416
|
if (!updatedResults || updatedResults.length === 0) {
|
|
409
417
|
throw new Error('no results');
|
|
410
418
|
}
|
|
411
419
|
if (debug && logger) {
|
|
412
|
-
logger(
|
|
420
|
+
logger(notModified
|
|
421
|
+
? `[DEBUG] Poll not modified (304), reusing ${updatedResults.length} cached results`
|
|
422
|
+
: `[DEBUG] Poll received ${updatedResults.length} results`);
|
|
413
423
|
for (const result of updatedResults) {
|
|
414
424
|
logger(`[DEBUG] Result status: ${result.test_file_name} - ${result.status}`);
|
|
415
425
|
}
|