-
Notifications
You must be signed in to change notification settings - Fork 53
Expand file tree
/
Copy pathresource_client.ts
More file actions
140 lines (125 loc) · 5 KB
/
Copy pathresource_client.ts
File metadata and controls
140 lines (125 loc) · 5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import type { ACT_JOB_STATUSES } from '@apify/consts';
import { ACT_JOB_TERMINAL_STATUSES } from '@apify/consts';
import type { z } from 'zod';
import type { ApifyApiError } from '../apify_api_error.js';
import type { ApifyRequestConfig } from '../http_client.js';
import { catchNotFoundForResourceOrThrow, catchNotFoundOrThrow, parseResponse } from '../utils.js';
import { ApiClient } from './api_client.js';
/**
* We need to supply some number for the API,
* because it would not accept "Infinity".
* 999999 seconds is more than 10 days.
*/
const MAX_WAIT_FOR_FINISH = 999999;
export const SMALL_TIMEOUT_MILLIS = 5 * 1000; // For fast and common actions. Suitable for idempotent actions.
export const MEDIUM_TIMEOUT_MILLIS = 30 * 1000; // For actions that may take longer.
export const DEFAULT_TIMEOUT_MILLIS = 360 * 1000; // 6 minutes
/**
* Resource client.
* @private
*/
export class ResourceClient extends ApiClient {
/**
* A 404 resolves to `undefined` only when the client names its resource by ID. A chained client without one, such
* as `run.dataset()`, throws it instead (see `catchNotFoundForResourceOrThrow()`).
*/
protected async _get<T, R>(
schema: z.ZodType,
options: T = {} as T,
timeoutMillis?: number,
): Promise<R | undefined> {
const requestOpts: ApifyRequestConfig = {
url: this._url(),
method: 'GET',
params: this._params(options),
timeout: timeoutMillis,
};
try {
const response = await this.httpClient.call(requestOpts);
return parseResponse<R>(response, schema);
} catch (err) {
catchNotFoundForResourceOrThrow(err as ApifyApiError, this.id);
}
return undefined;
}
protected async _update<T, R>(schema: z.ZodType, newFields: T, timeoutMillis?: number): Promise<R> {
const response = await this.httpClient.call({
url: this._url(),
method: 'PUT',
params: this._params(),
data: newFields,
timeout: timeoutMillis,
});
return parseResponse<R>(response, schema);
}
/**
* A 404 is swallowed, keeping the DELETE idempotent, only when the client names its resource by ID. A chained client
* without one throws it instead (see `catchNotFoundForResourceOrThrow()`).
*/
protected async _delete(timeoutMillis?: number): Promise<void> {
try {
await this.httpClient.call({
url: this._url(),
method: 'DELETE',
params: this._params(),
timeout: timeoutMillis,
});
} catch (err) {
catchNotFoundForResourceOrThrow(err as ApifyApiError, this.id);
}
}
/**
* This function is used in Build and Run endpoints so it's kept
* here to stay DRY.
*/
protected async _waitForFinish<R extends { status: (typeof ACT_JOB_STATUSES)[keyof typeof ACT_JOB_STATUSES] }>(
schema: z.ZodType,
options: WaitForFinishOptions = {},
): Promise<R> {
const { waitSecs = MAX_WAIT_FOR_FINISH } = options;
const waitMillis = waitSecs * 1000;
let job: R | undefined;
const startedAt = Date.now();
const shouldRepeat = () => {
const millisSinceStart = Date.now() - startedAt;
if (millisSinceStart >= waitMillis) return false;
const hasJobEnded =
job && ACT_JOB_TERMINAL_STATUSES.includes(job.status as (typeof ACT_JOB_TERMINAL_STATUSES)[number]);
return !hasJobEnded;
};
do {
const millisSinceStart = Date.now() - startedAt;
const remainingWaitSeconds = Math.round((waitMillis - millisSinceStart) / 1000);
const waitForFinish = Math.max(0, remainingWaitSeconds);
const requestOpts: ApifyRequestConfig = {
url: this._url(),
method: 'GET',
params: this._params({ waitForFinish }),
};
try {
const response = await this.httpClient.call(requestOpts);
job = parseResponse<R>(response, schema);
} catch (err) {
catchNotFoundOrThrow(err as ApifyApiError);
job = undefined;
}
// It might take some time for database replicas to get up-to-date,
// so getRun() might return null. Wait a little bit and try it again.
if (!job)
await new Promise((resolve) => {
setTimeout(resolve, 250);
});
} while (shouldRepeat());
if (!job) {
const constructorName = this.constructor.name;
const jobName = constructorName.match(/(\w+)Client/)![1].toLowerCase();
throw new Error(
`Waiting for ${jobName} to finish failed. Cannot fetch actor ${jobName} details from the server.`,
);
}
return job;
}
}
export interface WaitForFinishOptions {
waitSecs?: number;
}