diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 20588da37..18d70d0e8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -159,6 +159,7 @@ This project uses [oxlint](https://oxc.rs/docs/guide/usage/linter) and [oxfmt](h - Use single quotes for strings - Add trailing commas in multiline structures - Export types and interfaces alongside implementations +- Declare private members as `#private` identifiers, and use `protected` only for helpers that subclasses call - Avoid `any` types where possible (though the oxlint rule is disabled) ### Before Committing @@ -306,6 +307,7 @@ When adding support for a new API resource: // src/resource_clients/my_resource.ts import { ApiClientSubResourceOptions } from '../base/api_client'; import { ResourceClient } from '../base/resource_client'; +import * as schemas from '../schemas'; export class MyResourceClient extends ResourceClient { constructor(options: ApiClientSubResourceOptions) { @@ -316,15 +318,15 @@ export class MyResourceClient extends ResourceClient { } async get(): Promise { - return this._get(); + return this.getResource(schemas.MyResource()); } async update(newFields: MyResourceUpdate): Promise { - return this._update(newFields); + return this.updateResource(schemas.MyResource(), newFields); } async delete(): Promise { - return this._delete(); + return this.deleteResource(); } } ``` @@ -335,6 +337,7 @@ export class MyResourceClient extends ResourceClient { // src/resource_clients/my_resource_collection.ts import { ApiClientSubResourceOptions } from '../base/api_client'; import { ResourceCollectionClient } from '../base/resource_collection_client'; +import * as schemas from '../schemas'; import { PaginatedList } from '../utils'; export class MyResourceCollectionClient extends ResourceCollectionClient { @@ -346,11 +349,11 @@ export class MyResourceCollectionClient extends ResourceCollectionClient { } async list(options?: MyResourceListOptions): Promise> { - return this._list(options); + return this.listResources(schemas.ListOfMyResources(), options); } async create(resource: MyResourceCreate): Promise { - return this._create(resource); + return this.createResource(schemas.MyResource(), resource); } } ``` @@ -360,11 +363,11 @@ export class MyResourceCollectionClient extends ResourceCollectionClient { ```typescript // In src/apify_client.ts myResource(id: string): MyResourceClient { - return new MyResourceClient(this._subResourceOptions({ id })); + return new MyResourceClient({ id, ...this.subClientOptions() }); } myResources(): MyResourceCollectionClient { - return new MyResourceCollectionClient(this._options()); + return new MyResourceCollectionClient(this.subClientOptions()); } ``` diff --git a/docs/04_upgrading/upgrading_v3.md b/docs/04_upgrading/upgrading_v3.md index fd68e85f8..2195ca48e 100644 --- a/docs/04_upgrading/upgrading_v3.md +++ b/docs/04_upgrading/upgrading_v3.md @@ -368,3 +368,36 @@ export HTTPS_PROXY=http://proxy.example.com:3128 ``` The same `proxy-agent` upgrade removes the `[DEP0169] DeprecationWarning` about `url.parse()` that Node.js 24 and newer printed on the client's first request. + +## Non-public members no longer carry an underscore + +Members declared `private` or `protected` had a leading underscore on top of the keyword in v2, and the underscore is gone in v3. Code that calls only the public methods of a client is unaffected. A class that extends one of the client classes and calls a protected helper has to switch to the new name. The base classes below are not exported, so you reach these members by extending a concrete client such as `ActorClient`. + +| Class | v2 | v3 | +| --- | --- | --- | +| `ApiClient` | `_url()` | `buildUrl()` | +| `ApiClient` | `_publicUrl()` | `buildPublicUrl()` | +| `ApiClient` | `_params()` | `buildParams()` | +| `ApiClient` | `_subResourceOptions()` | `subResourceOptions()` | +| `ApiClient` | `_toSafeId()` | `toSafeId()` | +| `ApiClient` | `_listPaginatedFromCallback()` | `listPaginatedFromCallback()` | +| `ResourceClient` | `_get()` | `getResource()` | +| `ResourceClient` | `_update()` | `updateResource()` | +| `ResourceClient` | `_delete()` | `deleteResource()` | +| `ResourceClient` | `_waitForFinish()` | `waitForJobFinish()` | +| `ResourceCollectionClient` | `_list()` | `listResources()` | +| `ResourceCollectionClient` | `_listPaginated()` | `listResourcesPaginated()` | +| `ResourceCollectionClient` | `_create()` | `createResource()` | +| `ResourceCollectionClient` | `_getOrCreate()` | `getOrCreateResource()` | +| `RequestQueueClient` | `_batchAddRequests()` | `addRequestBatch()` | +| `RequestQueueClient` | `_batchAddRequestsWithRetries()` | `addRequestBatchWithRetries()` | + +A helper got a suffix wherever the bare name would collide with a public method of the same class, which is why `_get()` is now `getResource()` and not `get()`. + +`LoggerActorRedirect` keeps `_log()`, since it overrides the method of that name on the `Logger` base class in `@apify/log`. + +The `clientMethod` field on `ApifyApiError` is parsed from the stack trace, and a public method that delegates to one of these helpers is reported under the helper's name. An error from `client.actor(id).get()` says `ActorClient.getResource`, where v2 said `ActorClient.get`. Adjust anything that matches on these values in your logs. + +### Private members are private at runtime + +Members that v2 declared `private` are declared with a `#` in v3, so the JavaScript runtime enforces the boundary, where v2 relied on the type checker. Code that reached one of them through a cast, such as `(client.httpClient as any).nodeInitPromise`, throws a `TypeError` in v3. They also don't appear when you spread an instance, iterate `Object.keys()` on it, or pass it to `JSON.stringify()`. The `protected` helpers in the table keep the `protected` keyword, so a subclass can call them. diff --git a/src/apify_api_error.ts b/src/apify_api_error.ts index 145717faa..b0c3bae0c 100644 --- a/src/apify_api_error.ts +++ b/src/apify_api_error.ts @@ -18,23 +18,6 @@ export type { ApifyApiErrorType } from './models.js'; */ const CLIENT_METHOD_REGEX = /at( async)? ([A-Za-z]+(Collection)?Client)\.([A-Za-z]+) \(/; -/** - * A public method that returns the promise of a shared helper without awaiting it leaves only the helper on - * the stack, so the helper is reported under the method the caller invoked. - * @private - */ -const PUBLIC_METHOD_BY_HELPER: Record = { - getResource: 'get', - updateResource: 'update', - deleteResource: 'delete', - waitForJobFinish: 'waitForFinish', - listResources: 'list', - createResource: 'create', - getOrCreateResource: 'getOrCreate', - addRequestBatch: 'batchAddRequests', - addRequestBatchWithRetries: 'batchAddRequests', -}; - /** * An `ApifyApiError` is thrown for successful HTTP requests that reach the API, * but the API responds with an error response. Typically, those are rate limit @@ -53,8 +36,9 @@ export class ApifyApiError extends Error { override name: string; /** - * The invoked resource client and the method. Known issue: Sometimes it displays - * as `unknown` because it can't be parsed from a stack trace. + * The resource client and the method parsed from the stack trace, such as `ActorClient.getResource`. A public + * method that delegates to a shared helper of its class is reported under the helper. The value is `unknown` + * when the stack trace cannot be parsed. */ clientMethod: string; @@ -131,17 +115,17 @@ export class ApifyApiError extends Error { super(message); this.name = this.constructor.name; - this.clientMethod = this._extractClientAndMethodFromStack(); + this.clientMethod = this.#extractClientAndMethodFromStack(); this.statusCode = response.status; this.type = type; this.attempt = attempt; this.httpMethod = response.config?.method; - this.path = this._safelyParsePathFromResponse(response); + this.path = this.#safelyParsePathFromResponse(response); const stack = this.stack!; this.originalStack = stack.slice(stack.indexOf('\n')); - this.stack = this._createApiStack(); + this.stack = this.#createApiStack(); this.data = errorData; } @@ -156,7 +140,7 @@ export class ApifyApiError extends Error { return new ErrorClass(response, attempt); } - private _safelyParsePathFromResponse(response: AxiosResponse) { + #safelyParsePathFromResponse(response: AxiosResponse) { const urlString = response.config?.url; let url; try { @@ -167,10 +151,10 @@ export class ApifyApiError extends Error { return url.pathname + url.search; } - private _extractClientAndMethodFromStack() { + #extractClientAndMethodFromStack() { const match = this.stack!.match(CLIENT_METHOD_REGEX); if (!match) return 'unknown'; - return `${match[2]}.${PUBLIC_METHOD_BY_HELPER[match[4]] ?? match[4]}`; + return `${match[2]}.${match[4]}`; } /** @@ -187,7 +171,7 @@ export class ApifyApiError extends Error { * httpMethod: post * path: /v2/actor-tasks/user~my-task/runs */ - private _createApiStack() { + #createApiStack() { const { name, ...props } = this; const stack = Object.entries(props) diff --git a/src/apify_client.ts b/src/apify_client.ts index 9b3240242..fe1b92842 100644 --- a/src/apify_client.ts +++ b/src/apify_client.ts @@ -136,7 +136,7 @@ export class ApifyClient { }); } - private subClientOptions() { + #subClientOptions() { return { baseUrl: this.baseUrl, publicBaseUrl: this.publicBaseUrl, @@ -154,7 +154,7 @@ export class ApifyClient { * @see https://docs.apify.com/api/v2/acts-get */ actors(): ActorCollectionClient { - return new ActorCollectionClient(this.subClientOptions()); + return new ActorCollectionClient(this.#subClientOptions()); } /** @@ -178,7 +178,7 @@ export class ApifyClient { return new ActorClient({ id, - ...this.subClientOptions(), + ...this.#subClientOptions(), }); } @@ -191,7 +191,7 @@ export class ApifyClient { * @see https://docs.apify.com/api/v2/actor-builds-get */ builds(): BuildCollectionClient { - return new BuildCollectionClient(this.subClientOptions()); + return new BuildCollectionClient(this.#subClientOptions()); } /** @@ -208,7 +208,7 @@ export class ApifyClient { return new BuildClient({ id, - ...this.subClientOptions(), + ...this.#subClientOptions(), }); } @@ -221,7 +221,7 @@ export class ApifyClient { * @see https://docs.apify.com/api/v2/datasets-get */ datasets(): DatasetCollectionClient { - return new DatasetCollectionClient(this.subClientOptions()); + return new DatasetCollectionClient(this.#subClientOptions()); } /** @@ -254,7 +254,7 @@ export class ApifyClient { return new DatasetClient({ id, - ...this.subClientOptions(), + ...this.#subClientOptions(), }); } @@ -267,7 +267,7 @@ export class ApifyClient { * @see https://docs.apify.com/api/v2/key-value-stores-get */ keyValueStores(): KeyValueStoreCollectionClient { - return new KeyValueStoreCollectionClient(this.subClientOptions()); + return new KeyValueStoreCollectionClient(this.#subClientOptions()); } /** @@ -294,7 +294,7 @@ export class ApifyClient { return new KeyValueStoreClient({ id, - ...this.subClientOptions(), + ...this.#subClientOptions(), }); } @@ -310,7 +310,7 @@ export class ApifyClient { return new LogClient({ id: buildOrRunId, - ...this.subClientOptions(), + ...this.#subClientOptions(), }); } @@ -323,7 +323,7 @@ export class ApifyClient { * @see https://docs.apify.com/api/v2/request-queues-get */ requestQueues(): RequestQueueCollectionClient { - return new RequestQueueCollectionClient(this.subClientOptions()); + return new RequestQueueCollectionClient(this.#subClientOptions()); } /** @@ -353,7 +353,7 @@ export class ApifyClient { const apiClientOptions = { id, - ...this.subClientOptions(), + ...this.#subClientOptions(), }; return new RequestQueueClient(apiClientOptions, parsed); } @@ -368,7 +368,7 @@ export class ApifyClient { */ runs(): RunCollectionClient { return new RunCollectionClient({ - ...this.subClientOptions(), + ...this.#subClientOptions(), resourcePath: 'actor-runs', }); } @@ -397,7 +397,7 @@ export class ApifyClient { return new RunClient({ id, - ...this.subClientOptions(), + ...this.#subClientOptions(), }); } @@ -410,7 +410,7 @@ export class ApifyClient { * @see https://docs.apify.com/api/v2/actor-tasks-get */ tasks(): TaskCollectionClient { - return new TaskCollectionClient(this.subClientOptions()); + return new TaskCollectionClient(this.#subClientOptions()); } /** @@ -433,7 +433,7 @@ export class ApifyClient { return new TaskClient({ id, - ...this.subClientOptions(), + ...this.#subClientOptions(), }); } @@ -446,7 +446,7 @@ export class ApifyClient { * @see https://docs.apify.com/api/v2/schedules-get */ schedules(): ScheduleCollectionClient { - return new ScheduleCollectionClient(this.subClientOptions()); + return new ScheduleCollectionClient(this.#subClientOptions()); } /** @@ -463,7 +463,7 @@ export class ApifyClient { return new ScheduleClient({ id, - ...this.subClientOptions(), + ...this.#subClientOptions(), }); } @@ -481,7 +481,7 @@ export class ApifyClient { return new UserClient({ id, - ...this.subClientOptions(), + ...this.#subClientOptions(), }); } @@ -494,7 +494,7 @@ export class ApifyClient { * @see https://docs.apify.com/api/v2/webhooks-get */ webhooks(): WebhookCollectionClient { - return new WebhookCollectionClient(this.subClientOptions()); + return new WebhookCollectionClient(this.#subClientOptions()); } /** @@ -511,7 +511,7 @@ export class ApifyClient { return new WebhookClient({ id, - ...this.subClientOptions(), + ...this.#subClientOptions(), }); } @@ -524,7 +524,7 @@ export class ApifyClient { * @see https://docs.apify.com/api/v2/webhook-dispatches-get */ webhookDispatches(): WebhookDispatchCollectionClient { - return new WebhookDispatchCollectionClient(this.subClientOptions()); + return new WebhookDispatchCollectionClient(this.#subClientOptions()); } /** @@ -539,7 +539,7 @@ export class ApifyClient { return new WebhookDispatchClient({ id, - ...this.subClientOptions(), + ...this.#subClientOptions(), }); } @@ -552,7 +552,7 @@ export class ApifyClient { * @see https://docs.apify.com/api/v2/store-get */ store(): StoreCollectionClient { - return new StoreCollectionClient(this.subClientOptions()); + return new StoreCollectionClient(this.#subClientOptions()); } /** diff --git a/src/http_client.ts b/src/http_client.ts index b032744ed..bcf2c0e87 100644 --- a/src/http_client.ts +++ b/src/http_client.ts @@ -46,9 +46,9 @@ export class HttpClient { workflowKey?: string; - private nodeInitPromise?: Promise; + #nodeInitPromise?: Promise; - private userAgentSuffix?: string | string[]; + #userAgentSuffix?: string | string[]; constructor(options: HttpClientOptions) { const { token } = options; @@ -64,8 +64,7 @@ export class HttpClient { this.timeoutMaxMillis = options.timeoutMaxSecs * 1000; this.logger = options.logger; this.workflowKey = options.workflowKey || process.env[APIFY_ENV_VARS.WORKFLOW_KEY]; - this.userAgentSuffix = options.userAgentSuffix; - this.onRequestRetry = this.onRequestRetry.bind(this); + this.#userAgentSuffix = options.userAgentSuffix; this.axios = axios.create({ // Disable axios's built-in proxy handling since we're using custom agents @@ -117,15 +116,15 @@ export class HttpClient { responseInterceptors.forEach((i) => this.axios.interceptors.response.use(i as any)); } - private async ensureNodeInit(): Promise { + async #ensureNodeInit(): Promise { if (!isNode()) return; - this.nodeInitPromise ??= this.initNode(); + this.#nodeInitPromise ??= this.#initNode(); - return this.nodeInitPromise; + return this.#nodeInitPromise; } - private async initNode(): Promise { + async #initNode(): Promise { if (!isNode()) return; const [{ ProxyAgent }, os] = await Promise.all([import('proxy-agent'), import('node:os')]); @@ -173,26 +172,26 @@ export class HttpClient { const isAtHome = !!process.env[APIFY_ENV_VARS.IS_AT_HOME]; let userAgent = `ApifyClient/${version} (${os.platform()}; Node/${process.version}); isAtHome/${isAtHome}`; - if (this.userAgentSuffix) { - userAgent += `; ${asArray(this.userAgentSuffix).join('; ')}`; + if (this.#userAgentSuffix) { + userAgent += `; ${asArray(this.#userAgentSuffix).join('; ')}`; } this.axios.defaults.headers['User-Agent'] = userAgent; } async call(config: ApifyRequestConfig): Promise> { - await this.ensureNodeInit(); + await this.#ensureNodeInit(); this.stats.calls++; - const makeRequest = this.createRequestHandler(config); + const makeRequest = this.#createRequestHandler(config); return retry(makeRequest, { retries: this.maxRetries, minTimeout: this.minDelayBetweenRetriesMillis, - onRetry: this.onRequestRetry, + onRetry: (error, attempt) => this.#onRequestRetry(error, attempt), }); } - private informAboutStreamNoRetry() { + #informAboutStreamNoRetry() { this.logger.warningOnce( 'Request body was a stream - retrying will not work, as part of it was already consumed.', ); @@ -206,7 +205,7 @@ export class HttpClient { * status codes are retried. See the following functions for the * retrying logic. */ - private createRequestHandler(config: ApifyRequestConfig) { + #createRequestHandler(config: ApifyRequestConfig) { const { timeoutSecs = 'medium', ...axiosConfig } = config; const makeRequest: RetryFunction = async (stopTrying, attempt) => { @@ -224,11 +223,11 @@ export class HttpClient { response = await this.axios.request({ ...axiosConfig, - timeout: this.computeTimeoutMillis(timeoutSecs, attempt), + timeout: this.#computeTimeoutMillis(timeoutSecs, attempt), }); - if (this.isStatusOk(response.status)) return response; + if (this.#isStatusOk(response.status)) return response; } catch (err) { - return cast(this.handleRequestError(err as AxiosError, config, stopTrying)); + return cast(this.#handleRequestError(err as AxiosError, config, stopTrying)); } if (response.status === RATE_LIMIT_EXCEEDED_STATUS_CODE) { @@ -236,9 +235,9 @@ export class HttpClient { } const apiError = ApifyApiError.fromResponse(response, attempt); - if (this.isStatusCodeRetryable(response.status)) { + if (this.#isStatusCodeRetryable(response.status)) { if (requestIsStream) { - this.informAboutStreamNoRetry(); + this.#informAboutStreamNoRetry(); } else { // allow a retry throw apiError; @@ -252,7 +251,7 @@ export class HttpClient { return makeRequest; } - private isStatusOk(statusCode: number) { + #isStatusOk(statusCode: number) { return statusCode < 300; } @@ -262,7 +261,7 @@ export class HttpClient { * as no timeout. The result doubles with each attempt and is capped at `timeoutMaxMillis`. A requested * value above the cap is capped too, which warns once, since it does not take effect in full. */ - private computeTimeoutMillis(timeoutSecs: Timeout, attempt: number): number { + #computeTimeoutMillis(timeoutSecs: Timeout, attempt: number): number { if (timeoutSecs === 'noTimeout') return 0; const requestedMillis = typeof timeoutSecs === 'number' ? timeoutSecs * 1000 : this.timeoutMillis[timeoutSecs]; @@ -283,14 +282,14 @@ export class HttpClient { * Handles all unexpected errors that can happen, but are not * Apify API typed errors. E.g. network errors, timeouts and so on. */ - private handleRequestError(err: AxiosError, config: ApifyRequestConfig, stopTrying: (e: Error) => void) { - if (this.isTimeoutError(err) && config.doNotRetryTimeouts) { + #handleRequestError(err: AxiosError, config: ApifyRequestConfig, stopTrying: (e: Error) => void) { + if (this.#isTimeoutError(err) && config.doNotRetryTimeouts) { return stopTrying(err); } - if (this.isRetryableError(err)) { + if (this.#isRetryableError(err)) { if (isStream(config.data)) { - this.informAboutStreamNoRetry(); + this.#informAboutStreamNoRetry(); } else { throw err; } @@ -302,7 +301,7 @@ export class HttpClient { * Axios calls req.abort() on timeouts so timeout errors will * have a code ECONNABORTED. */ - private isTimeoutError(err: AxiosError) { + #isTimeoutError(err: AxiosError) { return err.code === 'ECONNABORTED'; } @@ -312,8 +311,8 @@ export class HttpClient { * @param {Error} err * @private */ - private isRetryableError(err: AxiosError) { - return this.isNetworkError(err) || this.isResponseBodyInvalid(err); + #isRetryableError(err: AxiosError) { + return this.#isNetworkError(err) || this.#isResponseBodyInvalid(err); } /** @@ -321,7 +320,7 @@ export class HttpClient { * a response, the request often does not fail, but simply contains * an incomplete response. This can often be fixed by retrying. */ - private isResponseBodyInvalid(err: Error): err is InvalidResponseBodyError { + #isResponseBodyInvalid(err: Error): err is InvalidResponseBodyError { return err instanceof InvalidResponseBodyError; } @@ -330,7 +329,7 @@ export class HttpClient { * it throws an AxiosError, which will have the request * and config (and other) properties. */ - private isNetworkError(err: AxiosError) { + #isNetworkError(err: AxiosError) { const hasRequest = err.request && typeof err.request === 'object'; const hasConfig = err.config && typeof err.config === 'object'; return hasRequest && hasConfig; @@ -341,13 +340,13 @@ export class HttpClient { * For status codes 300-499 (except 429) we do not retry the request, * because it's probably caused by invalid url (redirect 3xx) or invalid user input (4xx). */ - private isStatusCodeRetryable(statusCode: number) { + #isStatusCodeRetryable(statusCode: number) { const isRateLimitError = statusCode === RATE_LIMIT_EXCEEDED_STATUS_CODE; const isInternalError = statusCode >= 500; return isRateLimitError || isInternalError; } - private onRequestRetry(error: Error, attempt: number) { + #onRequestRetry(error: Error, attempt: number) { if (attempt === Math.round(this.maxRetries / 2)) { this.logger.warning( `API request failed ${attempt} times. Max attempts: ${this.maxRetries + 1}.\nCause:${error.stack}`, diff --git a/src/resource_clients/dataset.ts b/src/resource_clients/dataset.ts index a38121178..c3528afbc 100644 --- a/src/resource_clients/dataset.ts +++ b/src/resource_clients/dataset.ts @@ -230,7 +230,7 @@ export class DatasetClient< timeoutSecs, }); - return this.createPaginationList(response, datasetListOptions.desc ?? false); + return this.#createPaginationList(response, datasetListOptions.desc ?? false); }; return this.listPaginatedFromCallback(fetchItems, listOptions); @@ -435,7 +435,7 @@ export class DatasetClient< return createdItemsPublicUrl.toString(); } - private createPaginationList(response: ApifyResponse, userProvidedDesc: boolean): PaginatedList { + #createPaginationList(response: ApifyResponse, userProvidedDesc: boolean): PaginatedList { const page: PaginatedList = { items: response.data, total: Number(response.headers['x-apify-pagination-total']), diff --git a/src/resource_clients/log.ts b/src/resource_clients/log.ts index bb8d1b94a..e523014c0 100644 --- a/src/resource_clients/log.ts +++ b/src/resource_clients/log.ts @@ -163,73 +163,73 @@ export class LoggerActorRedirect extends Logger { * @since Added in 2.20.0 */ export class StreamedLog { - private destinationLog: Log; - private streamBuffer: Buffer[] = []; - private splitMarker = /(?:\n|^)(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)/g; - private relevancyTimeLimit: Date | null; + #destinationLog: Log; + #streamBuffer: Buffer[] = []; + #splitMarker = /(?:\n|^)(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)/g; + #relevancyTimeLimit: Date | null; - private logClient: LogClient; - private streamingTask: Promise | null = null; - private stopLogging = false; + #logClient: LogClient; + #streamingTask: Promise | null = null; + #stopLogging = false; constructor(options: StreamedLogOptions) { const { toLog, logClient, fromStart = true } = options; - this.destinationLog = toLog; - this.logClient = logClient; - this.relevancyTimeLimit = fromStart ? null : new Date(); + this.#destinationLog = toLog; + this.#logClient = logClient; + this.#relevancyTimeLimit = fromStart ? null : new Date(); } /** * Start log redirection. */ public start(): void { - if (this.streamingTask) { + if (this.#streamingTask) { throw new Error('Streaming task already active'); } - this.stopLogging = false; - this.streamingTask = this.streamLog(); + this.#stopLogging = false; + this.#streamingTask = this.#streamLog(); } /** * Stop log redirection. */ public async stop(): Promise { - if (!this.streamingTask) { + if (!this.#streamingTask) { throw new Error('Streaming task is not active'); } - this.stopLogging = true; + this.#stopLogging = true; try { - await this.streamingTask; + await this.#streamingTask; } catch (err) { if (!(err instanceof Error && err.name === 'AbortError')) { throw err; } } finally { - this.streamingTask = null; + this.#streamingTask = null; } } /** * Get log stream from response and redirect it to another log. */ - private async streamLog(): Promise { + async #streamLog(): Promise { try { - const logStream = await this.logClient.stream({ raw: true }); + const logStream = await this.#logClient.stream({ raw: true }); if (!logStream) { return; } - const lastChunkRemainder = await this.logStreamChunks(logStream); + const lastChunkRemainder = await this.#logStreamChunks(logStream); // Process whatever is left when exiting. Maybe it is incomplete, maybe it is last log without EOL. const lastMessage = Buffer.from(lastChunkRemainder).toString().trim(); if (lastMessage.length) { - this.destinationLog.info(lastMessage); + this.#destinationLog.info(lastMessage); } } catch (err) { log.warning(`Log redirection stopped due to error`, err as Error); } } - private async logStreamChunks(logStream: Readable): Promise { + async #logStreamChunks(logStream: Readable): Promise { // Chunk may be incomplete. Keep remainder for next chunk. let previousChunkRemainder: Uint8Array = new Uint8Array(); @@ -244,11 +244,11 @@ export class StreamedLog { previousChunkRemainder = chunkWithPreviousRemainder.slice(lastCompleteMessageIndex); // Push complete part of the chunk to the buffer - this.streamBuffer.push(Buffer.from(chunkWithPreviousRemainder.slice(0, lastCompleteMessageIndex))); - this.logBufferContent(); + this.#streamBuffer.push(Buffer.from(chunkWithPreviousRemainder.slice(0, lastCompleteMessageIndex))); + this.#logBufferContent(); // Keep processing the new data until stopped - if (this.stopLogging) { + if (this.#stopLogging) { break; } } @@ -258,20 +258,20 @@ export class StreamedLog { /** * Parse the buffer and log complete messages. */ - private logBufferContent(): void { - const allParts = Buffer.concat(this.streamBuffer).toString().split(this.splitMarker).slice(1); + #logBufferContent(): void { + const allParts = Buffer.concat(this.#streamBuffer).toString().split(this.#splitMarker).slice(1); // Parse the buffer parts into complete messages const messageMarkers = allParts.filter((_, i) => i % 2 === 0); const messageContents = allParts.filter((_, i) => i % 2 !== 0); - this.streamBuffer = []; + this.#streamBuffer = []; messageMarkers.forEach((marker, index) => { const decodedMarker = marker; const decodedContent = messageContents[index]; - if (this.relevancyTimeLimit) { + if (this.#relevancyTimeLimit) { // Log only relevant messages. Ignore too old log messages. const logTime = new Date(decodedMarker); - if (logTime < this.relevancyTimeLimit) { + if (logTime < this.#relevancyTimeLimit) { return; } } @@ -279,7 +279,7 @@ export class StreamedLog { // Original log level information is not available. Log all on info level. Log level could be guessed for // some logs, but for any multiline logs such guess would be probably correct only for the first line. - this.destinationLog.info(message.trim()); + this.#destinationLog.info(message.trim()); }); } } diff --git a/src/resource_clients/request_queue.ts b/src/resource_clients/request_queue.ts index 54e06ac69..78c5f74c9 100644 --- a/src/resource_clients/request_queue.ts +++ b/src/resource_clients/request_queue.ts @@ -145,9 +145,9 @@ export type { * @see https://docs.apify.com/platform/storage/request-queue */ export class RequestQueueClient extends ResourceClient { - private clientKey?: string; + #clientKey?: string; - private timeoutSecs?: number; + #timeoutSecs?: number; /** * @hidden @@ -158,19 +158,19 @@ export class RequestQueueClient extends ResourceClient { ...options, }); - this.clientKey = userOptions.clientKey; - this.timeoutSecs = userOptions.timeoutSecs; + this.#clientKey = userOptions.clientKey; + this.#timeoutSecs = userOptions.timeoutSecs; } /** * Picks the timeout of a request: an explicit per-call `timeoutSecs` as is, otherwise the method's default * tier, capped at the queue-wide `timeoutSecs` when one was given. */ - private resolveTimeout(timeoutSecs: Timeout | undefined, defaultTier: TimeoutTier): Timeout { + #resolveTimeout(timeoutSecs: Timeout | undefined, defaultTier: TimeoutTier): Timeout { if (timeoutSecs !== undefined) return timeoutSecs; - if (this.timeoutSecs === undefined) return defaultTier; + if (this.#timeoutSecs === undefined) return defaultTier; const tierSecs = this.httpClient.timeoutMillis[defaultTier] / 1000; - return tierSecs <= this.timeoutSecs ? defaultTier : this.timeoutSecs; + return tierSecs <= this.#timeoutSecs ? defaultTier : this.#timeoutSecs; } /** @@ -184,7 +184,7 @@ export class RequestQueueClient extends ResourceClient { async get(options: TimeoutOptions = {}): Promise { const { timeoutSecs } = parseArgument(options, timeoutOptionsSchema, 'TimeoutOptions'); - return this.getResource(schemas.RequestQueue(), {}, this.resolveTimeout(timeoutSecs, 'short')); + return this.getResource(schemas.RequestQueue(), {}, this.#resolveTimeout(timeoutSecs, 'short')); } /** @@ -200,7 +200,7 @@ export class RequestQueueClient extends ResourceClient { parseArgument(newFields, anyObjectSchema); const { timeoutSecs } = parseArgument(options, timeoutOptionsSchema, 'TimeoutOptions'); - return this.updateResource(schemas.RequestQueue(), newFields, this.resolveTimeout(timeoutSecs, 'short')); + return this.updateResource(schemas.RequestQueue(), newFields, this.#resolveTimeout(timeoutSecs, 'short')); } /** @@ -213,7 +213,7 @@ export class RequestQueueClient extends ResourceClient { async delete(options: TimeoutOptions = {}): Promise { const { timeoutSecs } = parseArgument(options, timeoutOptionsSchema, 'TimeoutOptions'); - return this.deleteResource(this.resolveTimeout(timeoutSecs, 'short')); + return this.deleteResource(this.#resolveTimeout(timeoutSecs, 'short')); } /** @@ -234,10 +234,10 @@ export class RequestQueueClient extends ResourceClient { const response = await this.httpClient.call({ url: this.buildUrl('head'), method: 'GET', - timeoutSecs: this.resolveTimeout(parsed.timeoutSecs, 'short'), + timeoutSecs: this.#resolveTimeout(parsed.timeoutSecs, 'short'), params: this.buildParams({ limit: parsed.limit, - clientKey: this.clientKey, + clientKey: this.#clientKey, }), }); @@ -286,11 +286,11 @@ export class RequestQueueClient extends ResourceClient { const response = await this.httpClient.call({ url: this.buildUrl('head/lock'), method: 'POST', - timeoutSecs: this.resolveTimeout(parsed.timeoutSecs, 'medium'), + timeoutSecs: this.#resolveTimeout(parsed.timeoutSecs, 'medium'), params: this.buildParams({ limit: parsed.limit, lockSecs: parsed.lockSecs, - clientKey: this.clientKey, + clientKey: this.#clientKey, }), }); @@ -346,11 +346,11 @@ export class RequestQueueClient extends ResourceClient { const response = await this.httpClient.call({ url: this.buildUrl('requests'), method: 'POST', - timeoutSecs: this.resolveTimeout(parsed.timeoutSecs, 'short'), + timeoutSecs: this.#resolveTimeout(parsed.timeoutSecs, 'short'), data: request, params: this.buildParams({ forefront: parsed.forefront, - clientKey: this.clientKey, + clientKey: this.#clientKey, }), }); @@ -372,11 +372,11 @@ export class RequestQueueClient extends ResourceClient { const response = await this.httpClient.call({ url: this.buildUrl('requests/batch'), method: 'POST', - timeoutSecs: this.resolveTimeout(parsed.timeoutSecs, 'medium'), + timeoutSecs: this.#resolveTimeout(parsed.timeoutSecs, 'medium'), data: requests, params: this.buildParams({ forefront: parsed.forefront, - clientKey: this.clientKey, + clientKey: this.#clientKey, }), }); @@ -575,10 +575,10 @@ export class RequestQueueClient extends ResourceClient { const response = await this.httpClient.call({ url: this.buildUrl('requests/batch'), method: 'DELETE', - timeoutSecs: this.resolveTimeout(timeoutSecs, 'short'), + timeoutSecs: this.#resolveTimeout(timeoutSecs, 'short'), data: requests, params: this.buildParams({ - clientKey: this.clientKey, + clientKey: this.#clientKey, }), }); @@ -604,7 +604,7 @@ export class RequestQueueClient extends ResourceClient { const requestOpts: ApifyRequestConfig = { url: this.buildUrl(['requests', id]), method: 'GET', - timeoutSecs: this.resolveTimeout(timeoutSecs, 'short'), + timeoutSecs: this.#resolveTimeout(timeoutSecs, 'short'), params: this.buildParams(), }; try { @@ -637,11 +637,11 @@ export class RequestQueueClient extends ResourceClient { const response = await this.httpClient.call({ url: this.buildUrl(['requests', request.id]), method: 'PUT', - timeoutSecs: this.resolveTimeout(parsed.timeoutSecs, 'medium'), + timeoutSecs: this.#resolveTimeout(parsed.timeoutSecs, 'medium'), data: request, params: this.buildParams({ forefront: parsed.forefront, - clientKey: this.clientKey, + clientKey: this.#clientKey, }), }); @@ -662,9 +662,9 @@ export class RequestQueueClient extends ResourceClient { await this.httpClient.call({ url: this.buildUrl(['requests', id]), method: 'DELETE', - timeoutSecs: this.resolveTimeout(timeoutSecs, 'short'), + timeoutSecs: this.#resolveTimeout(timeoutSecs, 'short'), params: this.buildParams({ - clientKey: this.clientKey, + clientKey: this.#clientKey, }), }); } @@ -709,11 +709,11 @@ export class RequestQueueClient extends ResourceClient { const response = await this.httpClient.call({ url: this.buildUrl(['requests', id, 'lock']), method: 'PUT', - timeoutSecs: this.resolveTimeout(parsed.timeoutSecs, 'medium'), + timeoutSecs: this.#resolveTimeout(parsed.timeoutSecs, 'medium'), params: this.buildParams({ forefront: parsed.forefront, lockSecs: parsed.lockSecs, - clientKey: this.clientKey, + clientKey: this.#clientKey, }), }); @@ -740,10 +740,10 @@ export class RequestQueueClient extends ResourceClient { await this.httpClient.call({ url: this.buildUrl(['requests', id, 'lock']), method: 'DELETE', - timeoutSecs: this.resolveTimeout(parsed.timeoutSecs, 'short'), + timeoutSecs: this.#resolveTimeout(parsed.timeoutSecs, 'short'), params: this.buildParams({ forefront: parsed.forefront, - clientKey: this.clientKey, + clientKey: this.#clientKey, }), }); } @@ -769,7 +769,7 @@ export class RequestQueueClient extends ResourceClient { listRequestsOptionsSchema, 'RequestQueueClientListRequestsOptions', ); - const pageTimeout = this.resolveTimeout(timeoutSecs, 'medium'); + const pageTimeout = this.#resolveTimeout(timeoutSecs, 'medium'); const getPaginatedList = async ( rqListOptions: RequestQueueClientListRequestsOptions = {}, @@ -781,7 +781,7 @@ export class RequestQueueClient extends ResourceClient { params: this.buildParams({ ...rqListOptions, filter: rqListOptions.filter ? rqListOptions.filter.join(',') : undefined, - clientKey: this.clientKey, + clientKey: this.#clientKey, }), }); @@ -838,9 +838,9 @@ export class RequestQueueClient extends ResourceClient { const response = await this.httpClient.call({ url: this.buildUrl('requests/unlock'), method: 'POST', - timeoutSecs: this.resolveTimeout(timeoutSecs, 'long'), + timeoutSecs: this.#resolveTimeout(timeoutSecs, 'long'), params: this.buildParams({ - clientKey: this.clientKey, + clientKey: this.#clientKey, }), }); diff --git a/src/statistics.ts b/src/statistics.ts index d19923989..d5c7a00c5 100644 --- a/src/statistics.ts +++ b/src/statistics.ts @@ -26,7 +26,7 @@ export class Statistics { // attempt is never 0, // but we don't want index 0 empty const index = attempt - 1; - this._fillBlanksWithZeroes(index); + this.#fillBlanksWithZeroes(index); this.rateLimitErrors[index]++; } @@ -35,7 +35,7 @@ export class Statistics { * number of zeroes by dynamically filling the empty indexes * when necessary. */ - private _fillBlanksWithZeroes(inclusiveIndex: number) { + #fillBlanksWithZeroes(inclusiveIndex: number) { if (this.rateLimitErrors.length <= inclusiveIndex) { for (let k = 0; k <= inclusiveIndex; k++) { if (typeof this.rateLimitErrors[k] !== 'number') { diff --git a/src/utils.ts b/src/utils.ts index 25e623edb..4b6abdc26 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -374,32 +374,30 @@ export function getVersionData(): { version: string } { * Helper class to create async iterators from paginated list endpoints. */ export class RequestQueuePaginationIterator { - private readonly maxPageLimit: number; + readonly #maxPageLimit: number; - private readonly getPage: ( - opts: RequestQueueClientListRequestsOptions, - ) => Promise; + readonly #getPage: (opts: RequestQueueClientListRequestsOptions) => Promise; - private readonly limit?: number; + readonly #limit?: number; - private readonly cursor?: string; + readonly #cursor?: string; constructor(options: RequestQueuePaginationIteratorOptions) { - this.maxPageLimit = options.maxPageLimit; - this.limit = options.limit; - this.cursor = options.cursor; - this.getPage = options.getPage; + this.#maxPageLimit = options.maxPageLimit; + this.#limit = options.limit; + this.#cursor = options.cursor; + this.#getPage = options.getPage; } async *[Symbol.asyncIterator](): AsyncIterator { - let nextCursor = this.cursor; + let nextCursor = this.#cursor; let iterateItemCount = 0; while (true) { - const pageLimit = this.limit - ? Math.min(this.maxPageLimit, this.limit - iterateItemCount) - : this.maxPageLimit; + const pageLimit = this.#limit + ? Math.min(this.#maxPageLimit, this.#limit - iterateItemCount) + : this.#maxPageLimit; - const page: RequestQueueClientListRequestsResult = await this.getPage({ + const page: RequestQueueClientListRequestsResult = await this.#getPage({ limit: pageLimit, cursor: nextCursor, }); @@ -408,7 +406,7 @@ export class RequestQueuePaginationIterator { yield page; iterateItemCount += page.items.length; // Limit reached stopping to iterate - if ((this.limit && iterateItemCount >= this.limit) || !page.nextCursor) return; + if ((this.#limit && iterateItemCount >= this.#limit) || !page.nextCursor) return; nextCursor = page.nextCursor; } diff --git a/test/apify_api_error.test.ts b/test/apify_api_error.test.ts index c9e4ba7cd..bbaf85460 100644 --- a/test/apify_api_error.test.ts +++ b/test/apify_api_error.test.ts @@ -46,7 +46,8 @@ describe('ApifyApiError', () => { // This does not work in v10 and lower, but we want to be able to run tests for v10, // because some people might still use it. They will just see clientMethod: undefined. if (!process.version.startsWith('v10')) { - expect(err.clientMethod).toBe(`${actorCollectionClient.constructor.name}.${method}`); + // `list()` returns the promise of the `listResources` helper, so the helper is the frame on the stack. + expect(err.clientMethod).toBe(`${actorCollectionClient.constructor.name}.listResources`); } expect(err.type).toEqual('token-not-provided'); expect(err.message).toEqual('Authentication token was not provided'); @@ -77,7 +78,7 @@ describe('ApifyApiError', () => { }, method); expect(error.name).toEqual('UnauthorizedError'); - expect(error.clientMethod).toBe(`ActorCollectionClient.${method}`); + expect(error.clientMethod).toBe('ActorCollectionClient.listResources'); expect(error.type).toEqual('token-not-provided'); expect(error.message).toEqual('Authentication token was not provided'); expect(error.statusCode).toEqual(401);