From 4478b551a8a8709a5001c925022214fcba6b8d3c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 05:57:20 +0000 Subject: [PATCH 1/7] fix: pass inherited run timeout to the API in seconds When starting another Actor run with `timeout: 'inherit'`, the remaining time of the current run was computed in milliseconds and passed directly to apify-client, whose `timeout` option is in seconds. The started run thus received a timeout 1000x longer than intended. The remaining time is now converted to seconds (rounded up, so that a sub-second remainder does not become 0, which means "no timeout") and clamped to a minimum of 0, matching the Python SDK behavior. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01E5mT2U8shnNpysaq8NdtZe --- src/actor.ts | 7 ++++--- test/apify/actor.test.ts | 25 +++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/actor.ts b/src/actor.ts index 8fcdc15c75..07f579f261 100644 --- a/src/actor.ts +++ b/src/actor.ts @@ -2299,13 +2299,14 @@ export class Actor { } /** - * Get time remaining from the Actor run timeout. Returns `undefined` if not on an Apify platform or the current - * run was started without a timeout. + * Get time remaining from the Actor run timeout, in seconds. Returns `undefined` if not on an Apify platform + * or the current run was started without a timeout. */ private getRemainingTime(): number | undefined { const env = this.getEnv(); if (this.isAtHome() && env.timeoutAt !== null) { - return env.timeoutAt.getTime() - Date.now(); + // Rounding up so that a sub-second remainder does not become 0, which the API treats as "no timeout". + return Math.max(Math.ceil((env.timeoutAt.getTime() - Date.now()) / 1000), 0); } log.warning( 'Using `inherit` argument is only possible when the Actor is running on the Apify platform and when the ' + diff --git a/test/apify/actor.test.ts b/test/apify/actor.test.ts index da10e0972e..ee8a48d395 100644 --- a/test/apify/actor.test.ts +++ b/test/apify/actor.test.ts @@ -930,7 +930,8 @@ describe('Actor', () => { const callSpy = vitest.spyOn(ActorClient.prototype, methodName).mockReturnValue(); await Actor[methodName](actId, input, options); expect(callSpy).toBeCalledWith(input, { - timeout: actorTimeout - usedTime, + // The client expects the timeout in seconds, while the remaining time is computed in milliseconds. + timeout: (actorTimeout - usedTime) / 1000, }); }, ); @@ -941,7 +942,27 @@ describe('Actor', () => { const callSpy = vitest.spyOn(TaskClient.prototype, 'call').mockReturnValue(); await Actor.callTask(actId, input, options); expect(callSpy).toBeCalledWith(input, { - timeout: actorTimeout - usedTime, + timeout: (actorTimeout - usedTime) / 1000, + }); + }); + + test(`inherited timeout is rounded up to a whole second`, async () => { + vi.setSystemTime(new Date(testStartTime.getTime() + usedTime + 500)); + + const callSpy = vitest.spyOn(ActorClient.prototype, 'call').mockReturnValue(); + await Actor.call(actId, input, { timeout: 'inherit' }); + expect(callSpy).toBeCalledWith(input, { + timeout: (actorTimeout - usedTime) / 1000, + }); + }); + + test(`inherited timeout is clamped to zero when the run is already past its timeout`, async () => { + vi.setSystemTime(new Date(testStartTime.getTime() + actorTimeout + 5000)); + + const callSpy = vitest.spyOn(ActorClient.prototype, 'call').mockReturnValue(); + await Actor.call(actId, input, { timeout: 'inherit' }); + expect(callSpy).toBeCalledWith(input, { + timeout: 0, }); }); }); From ba1e854d362f5beb6b6bf736ad3ecab99793e80b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 06:31:00 +0000 Subject: [PATCH 2/7] fix: skip starting another Actor when inherited timeout has no time remaining When `timeout: 'inherit'` is used and the current run is already past its own timeout, the resolved timeout would be 0, which the API treats as "no timeout" - the other Actor run would get unlimited runtime instead of inheriting the (exhausted) time budget. `Actor.start()`, `Actor.call()` and `Actor.callTask()` now skip the API call entirely in that case, log a warning and return `undefined`. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01E5mT2U8shnNpysaq8NdtZe --- src/actor.ts | 69 ++++++++++++++++++++++++++++++++++------ test/apify/actor.test.ts | 25 +++++++++++---- 2 files changed, 78 insertions(+), 16 deletions(-) diff --git a/src/actor.ts b/src/actor.ts index 07f579f261..17b5c9ac8c 100644 --- a/src/actor.ts +++ b/src/actor.ts @@ -252,6 +252,8 @@ export interface Timeout { * * Using `inherit` will set timeout of the newly started Actor run to the time * remaining until this Actor run times out so that the new run does not outlive this one. + * If there is no time remaining (this run is already past its own timeout), the new run + * is not started at all — the method only logs a warning and returns `undefined`. */ timeout?: number | 'inherit'; } @@ -748,8 +750,16 @@ export class Actor { * @param [options] * @ignore */ - async call(actorId: string, input?: unknown, options: CallOptions = {}): Promise { + async call(actorId: string, input?: unknown, options: CallOptions = {}): Promise { const timeout = options.timeout === 'inherit' ? this.getRemainingTime() : options.timeout; + + if (options.timeout === 'inherit' && timeout === 0) { + log.warning( + "Actor.call() was skipped: `timeout: 'inherit'` was used, but the current run has no time remaining before its own timeout.", + ); + return undefined; + } + const { token, ...rest } = options; const client = token ? this.newClient({ token }) : this.apifyClient; return client.actor(actorId).call(input, { ...rest, timeout }); @@ -779,8 +789,16 @@ export class Actor { * @param [options] * @ignore */ - async start(actorId: string, input?: unknown, options: StartOptions = {}): Promise { + async start(actorId: string, input?: unknown, options: StartOptions = {}): Promise { const timeout = options.timeout === 'inherit' ? this.getRemainingTime() : options.timeout; + + if (options.timeout === 'inherit' && timeout === 0) { + log.warning( + "Actor.start() was skipped: `timeout: 'inherit'` was used, but the current run has no time remaining before its own timeout.", + ); + return undefined; + } + const { token, ...rest } = options; const client = token ? this.newClient({ token }) : this.apifyClient; @@ -841,8 +859,20 @@ export class Actor { * @param [options] * @ignore */ - async callTask(taskId: string, input?: Dictionary, options: CallTaskOptions = {}): Promise { + async callTask( + taskId: string, + input?: Dictionary, + options: CallTaskOptions = {}, + ): Promise { const timeout = options.timeout === 'inherit' ? this.getRemainingTime() : options.timeout; + + if (options.timeout === 'inherit' && timeout === 0) { + log.warning( + "Actor.callTask() was skipped: `timeout: 'inherit'` was used, but the current run has no time remaining before its own timeout.", + ); + return undefined; + } + const { token, ...rest } = options; const client = token ? this.newClient({ token }) : this.apifyClient; @@ -1698,8 +1728,14 @@ export class Actor { * JSON and its content type set to `application/json; charset=utf-8`. * Otherwise the `options.contentType` parameter must be provided. * @param [options] - */ - static async call(actorId: string, input?: unknown, options: CallOptions = {}): Promise { + * @returns The Actor run object, or `undefined` when the `timeout: 'inherit'` option is used + * but the current run has no time remaining before its own timeout — the run is then not started at all. + */ + static async call( + actorId: string, + input?: unknown, + options: CallOptions = {}, + ): Promise { return Actor.getDefaultInstance().call(actorId, input, options); } @@ -1727,8 +1763,14 @@ export class Actor { * JSON and its content type set to `application/json; charset=utf-8`. * Provided input will be merged with Actor task input. * @param [options] - */ - static async callTask(taskId: string, input?: Dictionary, options: CallTaskOptions = {}): Promise { + * @returns The Actor run object, or `undefined` when the `timeout: 'inherit'` option is used + * but the current run has no time remaining before its own timeout — the run is then not started at all. + */ + static async callTask( + taskId: string, + input?: Dictionary, + options: CallTaskOptions = {}, + ): Promise { return Actor.getDefaultInstance().callTask(taskId, input, options); } @@ -1754,8 +1796,14 @@ export class Actor { * JSON and its content type set to `application/json; charset=utf-8`. * Otherwise the `options.contentType` parameter must be provided. * @param [options] - */ - static async start(actorId: string, input?: Dictionary, options: StartOptions = {}): Promise { + * @returns The Actor run object, or `undefined` when the `timeout: 'inherit'` option is used + * but the current run has no time remaining before its own timeout — the run is then not started at all. + */ + static async start( + actorId: string, + input?: Dictionary, + options: StartOptions = {}, + ): Promise { return Actor.getDefaultInstance().start(actorId, input, options); } @@ -2305,7 +2353,8 @@ export class Actor { private getRemainingTime(): number | undefined { const env = this.getEnv(); if (this.isAtHome() && env.timeoutAt !== null) { - // Rounding up so that a sub-second remainder does not become 0, which the API treats as "no timeout". + // Rounded up so that a positive remainder is never truncated to 0 — callers treat 0 (only possible + // when this run is already past its own timeout) as "no time remaining" and skip starting the run. return Math.max(Math.ceil((env.timeoutAt.getTime() - Date.now()) / 1000), 0); } log.warning( diff --git a/test/apify/actor.test.ts b/test/apify/actor.test.ts index ee8a48d395..2634ca7b2c 100644 --- a/test/apify/actor.test.ts +++ b/test/apify/actor.test.ts @@ -956,14 +956,27 @@ describe('Actor', () => { }); }); - test(`inherited timeout is clamped to zero when the run is already past its timeout`, async () => { + test.each([{ methodName: 'call' }, { methodName: 'start' }])( + `Actor.$methodName({timeout: 'inherit'}) is skipped when the run is already past its timeout`, + async ({ methodName }) => { + vi.setSystemTime(new Date(testStartTime.getTime() + actorTimeout + 5000)); + + const warningSpy = vitest.spyOn(log, 'warning').mockImplementation(() => {}); + const callSpy = vitest.spyOn(ActorClient.prototype, methodName).mockReturnValue(); + await expect(Actor[methodName](actId, input, { timeout: 'inherit' })).resolves.toBeUndefined(); + expect(callSpy).not.toBeCalled(); + expect(warningSpy).toBeCalledWith(expect.stringContaining('skipped')); + }, + ); + + test(`Actor.callTask({timeout: 'inherit'}) is skipped when the run is already past its timeout`, async () => { vi.setSystemTime(new Date(testStartTime.getTime() + actorTimeout + 5000)); - const callSpy = vitest.spyOn(ActorClient.prototype, 'call').mockReturnValue(); - await Actor.call(actId, input, { timeout: 'inherit' }); - expect(callSpy).toBeCalledWith(input, { - timeout: 0, - }); + const warningSpy = vitest.spyOn(log, 'warning').mockImplementation(() => {}); + const callSpy = vitest.spyOn(TaskClient.prototype, 'call').mockReturnValue(); + await expect(Actor.callTask(actId, input, { timeout: 'inherit' })).resolves.toBeUndefined(); + expect(callSpy).not.toBeCalled(); + expect(warningSpy).toBeCalledWith(expect.stringContaining('skipped')); }); }); From 708e1480406ae27eb30d78bf632ae1ce57cd6429 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 06:41:52 +0000 Subject: [PATCH 3/7] test: drop log message asserts from inherit-timeout skip tests Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01E5mT2U8shnNpysaq8NdtZe --- test/apify/actor.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/apify/actor.test.ts b/test/apify/actor.test.ts index 2634ca7b2c..9eac771d26 100644 --- a/test/apify/actor.test.ts +++ b/test/apify/actor.test.ts @@ -961,22 +961,20 @@ describe('Actor', () => { async ({ methodName }) => { vi.setSystemTime(new Date(testStartTime.getTime() + actorTimeout + 5000)); - const warningSpy = vitest.spyOn(log, 'warning').mockImplementation(() => {}); + vitest.spyOn(log, 'warning').mockImplementation(() => {}); const callSpy = vitest.spyOn(ActorClient.prototype, methodName).mockReturnValue(); await expect(Actor[methodName](actId, input, { timeout: 'inherit' })).resolves.toBeUndefined(); expect(callSpy).not.toBeCalled(); - expect(warningSpy).toBeCalledWith(expect.stringContaining('skipped')); }, ); test(`Actor.callTask({timeout: 'inherit'}) is skipped when the run is already past its timeout`, async () => { vi.setSystemTime(new Date(testStartTime.getTime() + actorTimeout + 5000)); - const warningSpy = vitest.spyOn(log, 'warning').mockImplementation(() => {}); + vitest.spyOn(log, 'warning').mockImplementation(() => {}); const callSpy = vitest.spyOn(TaskClient.prototype, 'call').mockReturnValue(); await expect(Actor.callTask(actId, input, { timeout: 'inherit' })).resolves.toBeUndefined(); expect(callSpy).not.toBeCalled(); - expect(warningSpy).toBeCalledWith(expect.stringContaining('skipped')); }); }); From bb068c59574793643dd2613196f5f4e1384ebaed Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 07:31:07 +0000 Subject: [PATCH 4/7] refactor: use overloads so non-inherit timeouts keep a non-optional run result `Actor.start()`, `Actor.call()` and `Actor.callTask()` (instance and static) now declare three overloads: calls with a numeric or omitted timeout keep returning `Promise`, `timeout: 'inherit'` returns `Promise` to surface the possible skip, and a catch-all signature keeps un-narrowed options unions honest with the `| undefined` return type. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01E5mT2U8shnNpysaq8NdtZe --- src/actor.ts | 125 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 119 insertions(+), 6 deletions(-) diff --git a/src/actor.ts b/src/actor.ts index 17b5c9ac8c..a6daa64ef9 100644 --- a/src/actor.ts +++ b/src/actor.ts @@ -750,6 +750,15 @@ export class Actor { * @param [options] * @ignore */ + async call(actorId: string, input?: unknown, options?: CallOptions & { timeout?: number }): Promise; + /** @ignore */ + async call( + actorId: string, + input: unknown, + options: CallOptions & { timeout: 'inherit' }, + ): Promise; + /** @ignore */ + async call(actorId: string, input?: unknown, options?: CallOptions): Promise; async call(actorId: string, input?: unknown, options: CallOptions = {}): Promise { const timeout = options.timeout === 'inherit' ? this.getRemainingTime() : options.timeout; @@ -789,6 +798,19 @@ export class Actor { * @param [options] * @ignore */ + async start( + actorId: string, + input?: unknown, + options?: StartOptions & { timeout?: number }, + ): Promise; + /** @ignore */ + async start( + actorId: string, + input: unknown, + options: StartOptions & { timeout: 'inherit' }, + ): Promise; + /** @ignore */ + async start(actorId: string, input?: unknown, options?: StartOptions): Promise; async start(actorId: string, input?: unknown, options: StartOptions = {}): Promise { const timeout = options.timeout === 'inherit' ? this.getRemainingTime() : options.timeout; @@ -859,6 +881,19 @@ export class Actor { * @param [options] * @ignore */ + async callTask( + taskId: string, + input?: Dictionary, + options?: CallTaskOptions & { timeout?: number }, + ): Promise; + /** @ignore */ + async callTask( + taskId: string, + input: Dictionary | undefined, + options: CallTaskOptions & { timeout: 'inherit' }, + ): Promise; + /** @ignore */ + async callTask(taskId: string, input?: Dictionary, options?: CallTaskOptions): Promise; async callTask( taskId: string, input?: Dictionary, @@ -1728,9 +1763,32 @@ export class Actor { * JSON and its content type set to `application/json; charset=utf-8`. * Otherwise the `options.contentType` parameter must be provided. * @param [options] - * @returns The Actor run object, or `undefined` when the `timeout: 'inherit'` option is used - * but the current run has no time remaining before its own timeout — the run is then not started at all. + * @returns The Actor run object. + */ + static async call( + actorId: string, + input?: unknown, + options?: CallOptions & { timeout?: number }, + ): Promise; + /** + * Runs an Actor on the Apify platform with the timeout inherited from the current run, so that + * the new run does not outlive this one. + * + * @returns The Actor run object, or `undefined` when the current run has no time remaining before + * its own timeout — the run is then not started at all and only a warning is logged. */ + static async call( + actorId: string, + input: unknown, + options: CallOptions & { timeout: 'inherit' }, + ): Promise; + /** + * Runs an Actor on the Apify platform using the current user account (determined by the `APIFY_TOKEN` environment variable). + * + * @returns The Actor run object. It is `undefined` only when `options.timeout` resolves to `'inherit'` + * and the current run has no time remaining before its own timeout. + */ + static async call(actorId: string, input?: unknown, options?: CallOptions): Promise; static async call( actorId: string, input?: unknown, @@ -1763,9 +1821,36 @@ export class Actor { * JSON and its content type set to `application/json; charset=utf-8`. * Provided input will be merged with Actor task input. * @param [options] - * @returns The Actor run object, or `undefined` when the `timeout: 'inherit'` option is used - * but the current run has no time remaining before its own timeout — the run is then not started at all. + * @returns The Actor run object. + */ + static async callTask( + taskId: string, + input?: Dictionary, + options?: CallTaskOptions & { timeout?: number }, + ): Promise; + /** + * Runs an Actor task on the Apify platform with the timeout inherited from the current run, so that + * the new run does not outlive this one. + * + * @returns The Actor run object, or `undefined` when the current run has no time remaining before + * its own timeout — the run is then not started at all and only a warning is logged. + */ + static async callTask( + taskId: string, + input: Dictionary | undefined, + options: CallTaskOptions & { timeout: 'inherit' }, + ): Promise; + /** + * Runs an Actor task on the Apify platform using the current user account (determined by the `APIFY_TOKEN` environment variable). + * + * @returns The Actor run object. It is `undefined` only when `options.timeout` resolves to `'inherit'` + * and the current run has no time remaining before its own timeout. */ + static async callTask( + taskId: string, + input?: Dictionary, + options?: CallTaskOptions, + ): Promise; static async callTask( taskId: string, input?: Dictionary, @@ -1796,9 +1881,37 @@ export class Actor { * JSON and its content type set to `application/json; charset=utf-8`. * Otherwise the `options.contentType` parameter must be provided. * @param [options] - * @returns The Actor run object, or `undefined` when the `timeout: 'inherit'` option is used - * but the current run has no time remaining before its own timeout — the run is then not started at all. + * @returns The Actor run object. + */ + static async start( + actorId: string, + input?: Dictionary, + options?: StartOptions & { timeout?: number }, + ): Promise; + /** + * Starts an Actor run on the Apify platform with the timeout inherited from the current run, so that + * the new run does not outlive this one. + * + * @returns The Actor run object, or `undefined` when the current run has no time remaining before + * its own timeout — the run is then not started at all and only a warning is logged. */ + static async start( + actorId: string, + input: Dictionary | undefined, + options: StartOptions & { timeout: 'inherit' }, + ): Promise; + /** + * Runs an Actor on the Apify platform using the current user account (determined by the `APIFY_TOKEN` environment variable), + * unlike `Actor.call`, this method just starts the run without waiting for finish. + * + * @returns The Actor run object. It is `undefined` only when `options.timeout` resolves to `'inherit'` + * and the current run has no time remaining before its own timeout. + */ + static async start( + actorId: string, + input?: Dictionary, + options?: StartOptions, + ): Promise; static async start( actorId: string, input?: Dictionary, From 7aeee2ad0d17bcfb4ea67f83ab86e4c53968ae30 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 11:31:24 +0000 Subject: [PATCH 5/7] fix: clamp inherited timeout to minimum of 1 second instead of skipping the run Match the approach merged in the Python SDK (apify/apify-sdk-python#1051): when `timeout: 'inherit'` is used and the current run is already past its own timeout, the started run now gets the smallest API-accepted timeout of 1 second (resulting in an almost certain timeout of the started run) instead of being skipped. The API treats a timeout of 0 as no timeout at all, which is why the value is never allowed to reach 0. This removes the skip behavior, the `| undefined` return types and the overloads - `Actor.start()`, `Actor.call()` and `Actor.callTask()` always return the run object again. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01E5mT2U8shnNpysaq8NdtZe --- src/actor.ts | 185 +++------------------------------------ test/apify/actor.test.ts | 18 ++-- 2 files changed, 23 insertions(+), 180 deletions(-) diff --git a/src/actor.ts b/src/actor.ts index a6daa64ef9..23d29e4670 100644 --- a/src/actor.ts +++ b/src/actor.ts @@ -252,8 +252,6 @@ export interface Timeout { * * Using `inherit` will set timeout of the newly started Actor run to the time * remaining until this Actor run times out so that the new run does not outlive this one. - * If there is no time remaining (this run is already past its own timeout), the new run - * is not started at all — the method only logs a warning and returns `undefined`. */ timeout?: number | 'inherit'; } @@ -750,25 +748,8 @@ export class Actor { * @param [options] * @ignore */ - async call(actorId: string, input?: unknown, options?: CallOptions & { timeout?: number }): Promise; - /** @ignore */ - async call( - actorId: string, - input: unknown, - options: CallOptions & { timeout: 'inherit' }, - ): Promise; - /** @ignore */ - async call(actorId: string, input?: unknown, options?: CallOptions): Promise; - async call(actorId: string, input?: unknown, options: CallOptions = {}): Promise { + async call(actorId: string, input?: unknown, options: CallOptions = {}): Promise { const timeout = options.timeout === 'inherit' ? this.getRemainingTime() : options.timeout; - - if (options.timeout === 'inherit' && timeout === 0) { - log.warning( - "Actor.call() was skipped: `timeout: 'inherit'` was used, but the current run has no time remaining before its own timeout.", - ); - return undefined; - } - const { token, ...rest } = options; const client = token ? this.newClient({ token }) : this.apifyClient; return client.actor(actorId).call(input, { ...rest, timeout }); @@ -798,29 +779,8 @@ export class Actor { * @param [options] * @ignore */ - async start( - actorId: string, - input?: unknown, - options?: StartOptions & { timeout?: number }, - ): Promise; - /** @ignore */ - async start( - actorId: string, - input: unknown, - options: StartOptions & { timeout: 'inherit' }, - ): Promise; - /** @ignore */ - async start(actorId: string, input?: unknown, options?: StartOptions): Promise; - async start(actorId: string, input?: unknown, options: StartOptions = {}): Promise { + async start(actorId: string, input?: unknown, options: StartOptions = {}): Promise { const timeout = options.timeout === 'inherit' ? this.getRemainingTime() : options.timeout; - - if (options.timeout === 'inherit' && timeout === 0) { - log.warning( - "Actor.start() was skipped: `timeout: 'inherit'` was used, but the current run has no time remaining before its own timeout.", - ); - return undefined; - } - const { token, ...rest } = options; const client = token ? this.newClient({ token }) : this.apifyClient; @@ -881,33 +841,8 @@ export class Actor { * @param [options] * @ignore */ - async callTask( - taskId: string, - input?: Dictionary, - options?: CallTaskOptions & { timeout?: number }, - ): Promise; - /** @ignore */ - async callTask( - taskId: string, - input: Dictionary | undefined, - options: CallTaskOptions & { timeout: 'inherit' }, - ): Promise; - /** @ignore */ - async callTask(taskId: string, input?: Dictionary, options?: CallTaskOptions): Promise; - async callTask( - taskId: string, - input?: Dictionary, - options: CallTaskOptions = {}, - ): Promise { + async callTask(taskId: string, input?: Dictionary, options: CallTaskOptions = {}): Promise { const timeout = options.timeout === 'inherit' ? this.getRemainingTime() : options.timeout; - - if (options.timeout === 'inherit' && timeout === 0) { - log.warning( - "Actor.callTask() was skipped: `timeout: 'inherit'` was used, but the current run has no time remaining before its own timeout.", - ); - return undefined; - } - const { token, ...rest } = options; const client = token ? this.newClient({ token }) : this.apifyClient; @@ -1763,37 +1698,8 @@ export class Actor { * JSON and its content type set to `application/json; charset=utf-8`. * Otherwise the `options.contentType` parameter must be provided. * @param [options] - * @returns The Actor run object. - */ - static async call( - actorId: string, - input?: unknown, - options?: CallOptions & { timeout?: number }, - ): Promise; - /** - * Runs an Actor on the Apify platform with the timeout inherited from the current run, so that - * the new run does not outlive this one. - * - * @returns The Actor run object, or `undefined` when the current run has no time remaining before - * its own timeout — the run is then not started at all and only a warning is logged. - */ - static async call( - actorId: string, - input: unknown, - options: CallOptions & { timeout: 'inherit' }, - ): Promise; - /** - * Runs an Actor on the Apify platform using the current user account (determined by the `APIFY_TOKEN` environment variable). - * - * @returns The Actor run object. It is `undefined` only when `options.timeout` resolves to `'inherit'` - * and the current run has no time remaining before its own timeout. */ - static async call(actorId: string, input?: unknown, options?: CallOptions): Promise; - static async call( - actorId: string, - input?: unknown, - options: CallOptions = {}, - ): Promise { + static async call(actorId: string, input?: unknown, options: CallOptions = {}): Promise { return Actor.getDefaultInstance().call(actorId, input, options); } @@ -1821,41 +1727,8 @@ export class Actor { * JSON and its content type set to `application/json; charset=utf-8`. * Provided input will be merged with Actor task input. * @param [options] - * @returns The Actor run object. */ - static async callTask( - taskId: string, - input?: Dictionary, - options?: CallTaskOptions & { timeout?: number }, - ): Promise; - /** - * Runs an Actor task on the Apify platform with the timeout inherited from the current run, so that - * the new run does not outlive this one. - * - * @returns The Actor run object, or `undefined` when the current run has no time remaining before - * its own timeout — the run is then not started at all and only a warning is logged. - */ - static async callTask( - taskId: string, - input: Dictionary | undefined, - options: CallTaskOptions & { timeout: 'inherit' }, - ): Promise; - /** - * Runs an Actor task on the Apify platform using the current user account (determined by the `APIFY_TOKEN` environment variable). - * - * @returns The Actor run object. It is `undefined` only when `options.timeout` resolves to `'inherit'` - * and the current run has no time remaining before its own timeout. - */ - static async callTask( - taskId: string, - input?: Dictionary, - options?: CallTaskOptions, - ): Promise; - static async callTask( - taskId: string, - input?: Dictionary, - options: CallTaskOptions = {}, - ): Promise { + static async callTask(taskId: string, input?: Dictionary, options: CallTaskOptions = {}): Promise { return Actor.getDefaultInstance().callTask(taskId, input, options); } @@ -1881,42 +1754,8 @@ export class Actor { * JSON and its content type set to `application/json; charset=utf-8`. * Otherwise the `options.contentType` parameter must be provided. * @param [options] - * @returns The Actor run object. */ - static async start( - actorId: string, - input?: Dictionary, - options?: StartOptions & { timeout?: number }, - ): Promise; - /** - * Starts an Actor run on the Apify platform with the timeout inherited from the current run, so that - * the new run does not outlive this one. - * - * @returns The Actor run object, or `undefined` when the current run has no time remaining before - * its own timeout — the run is then not started at all and only a warning is logged. - */ - static async start( - actorId: string, - input: Dictionary | undefined, - options: StartOptions & { timeout: 'inherit' }, - ): Promise; - /** - * Runs an Actor on the Apify platform using the current user account (determined by the `APIFY_TOKEN` environment variable), - * unlike `Actor.call`, this method just starts the run without waiting for finish. - * - * @returns The Actor run object. It is `undefined` only when `options.timeout` resolves to `'inherit'` - * and the current run has no time remaining before its own timeout. - */ - static async start( - actorId: string, - input?: Dictionary, - options?: StartOptions, - ): Promise; - static async start( - actorId: string, - input?: Dictionary, - options: StartOptions = {}, - ): Promise { + static async start(actorId: string, input?: Dictionary, options: StartOptions = {}): Promise { return Actor.getDefaultInstance().start(actorId, input, options); } @@ -2460,15 +2299,17 @@ export class Actor { } /** - * Get time remaining from the Actor run timeout, in seconds. Returns `undefined` if not on an Apify platform - * or the current run was started without a timeout. + * Get time remaining from the Actor run timeout, rounded up to whole seconds with minimum value of 1 second. + * + * The API treats a 0 second timeout as no timeout, the minimum acceptable timeout is 1 second. + * + * Returns `undefined` if not on an Apify platform or the current run was started without a timeout. */ private getRemainingTime(): number | undefined { const env = this.getEnv(); + const smallestPossibleApiTimeout = 1; if (this.isAtHome() && env.timeoutAt !== null) { - // Rounded up so that a positive remainder is never truncated to 0 — callers treat 0 (only possible - // when this run is already past its own timeout) as "no time remaining" and skip starting the run. - return Math.max(Math.ceil((env.timeoutAt.getTime() - Date.now()) / 1000), 0); + return Math.max(Math.ceil((env.timeoutAt.getTime() - Date.now()) / 1000), smallestPossibleApiTimeout); } log.warning( 'Using `inherit` argument is only possible when the Actor is running on the Apify platform and when the ' + diff --git a/test/apify/actor.test.ts b/test/apify/actor.test.ts index 9eac771d26..cbe8719faa 100644 --- a/test/apify/actor.test.ts +++ b/test/apify/actor.test.ts @@ -957,24 +957,26 @@ describe('Actor', () => { }); test.each([{ methodName: 'call' }, { methodName: 'start' }])( - `Actor.$methodName({timeout: 'inherit'}) is skipped when the run is already past its timeout`, + `Actor.$methodName({timeout: 'inherit'}) is clamped to 1 second when the run is already past its timeout`, async ({ methodName }) => { vi.setSystemTime(new Date(testStartTime.getTime() + actorTimeout + 5000)); - vitest.spyOn(log, 'warning').mockImplementation(() => {}); const callSpy = vitest.spyOn(ActorClient.prototype, methodName).mockReturnValue(); - await expect(Actor[methodName](actId, input, { timeout: 'inherit' })).resolves.toBeUndefined(); - expect(callSpy).not.toBeCalled(); + await Actor[methodName](actId, input, { timeout: 'inherit' }); + expect(callSpy).toBeCalledWith(input, { + timeout: 1, + }); }, ); - test(`Actor.callTask({timeout: 'inherit'}) is skipped when the run is already past its timeout`, async () => { + test(`Actor.callTask({timeout: 'inherit'}) is clamped to 1 second when the run is already past its timeout`, async () => { vi.setSystemTime(new Date(testStartTime.getTime() + actorTimeout + 5000)); - vitest.spyOn(log, 'warning').mockImplementation(() => {}); const callSpy = vitest.spyOn(TaskClient.prototype, 'call').mockReturnValue(); - await expect(Actor.callTask(actId, input, { timeout: 'inherit' })).resolves.toBeUndefined(); - expect(callSpy).not.toBeCalled(); + await Actor.callTask(actId, input, { timeout: 'inherit' }); + expect(callSpy).toBeCalledWith(input, { + timeout: 1, + }); }); }); From 16d10c9481020bc90173a871ee03722c6d26a0d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Proch=C3=A1zka?= Date: Thu, 16 Jul 2026 14:51:07 +0200 Subject: [PATCH 6/7] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jindřich Bär --- src/actor.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/actor.ts b/src/actor.ts index 23d29e4670..e5014eda58 100644 --- a/src/actor.ts +++ b/src/actor.ts @@ -2299,15 +2299,15 @@ export class Actor { } /** - * Get time remaining from the Actor run timeout, rounded up to whole seconds with minimum value of 1 second. + * Get time remaining from the Actor run timeout in seconds, rounded up to whole seconds with minimum value of 1 second. * * The API treats a 0 second timeout as no timeout, the minimum acceptable timeout is 1 second. * - * Returns `undefined` if not on an Apify platform or the current run was started without a timeout. + * Returns `undefined` if not on the Apify platform or the current run was started without a timeout. */ - private getRemainingTime(): number | undefined { + private getRemainingTimeSecs(): number | undefined { const env = this.getEnv(); - const smallestPossibleApiTimeout = 1; + const MINIMUM_API_TIMEOUT_SECS = 1; if (this.isAtHome() && env.timeoutAt !== null) { return Math.max(Math.ceil((env.timeoutAt.getTime() - Date.now()) / 1000), smallestPossibleApiTimeout); } From be3d0471d665009d70b6a6c65075a01cc28c8a0a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 12:54:25 +0000 Subject: [PATCH 7/7] fix: update references left broken by applied review suggestions The review suggestions renamed `getRemainingTime` to `getRemainingTimeSecs` and the local constant to `MINIMUM_API_TIMEOUT_SECS`, but the call sites still used the old names. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01E5mT2U8shnNpysaq8NdtZe --- src/actor.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/actor.ts b/src/actor.ts index e5014eda58..d0623f0d90 100644 --- a/src/actor.ts +++ b/src/actor.ts @@ -749,7 +749,7 @@ export class Actor { * @ignore */ async call(actorId: string, input?: unknown, options: CallOptions = {}): Promise { - const timeout = options.timeout === 'inherit' ? this.getRemainingTime() : options.timeout; + const timeout = options.timeout === 'inherit' ? this.getRemainingTimeSecs() : options.timeout; const { token, ...rest } = options; const client = token ? this.newClient({ token }) : this.apifyClient; return client.actor(actorId).call(input, { ...rest, timeout }); @@ -780,7 +780,7 @@ export class Actor { * @ignore */ async start(actorId: string, input?: unknown, options: StartOptions = {}): Promise { - const timeout = options.timeout === 'inherit' ? this.getRemainingTime() : options.timeout; + const timeout = options.timeout === 'inherit' ? this.getRemainingTimeSecs() : options.timeout; const { token, ...rest } = options; const client = token ? this.newClient({ token }) : this.apifyClient; @@ -842,7 +842,7 @@ export class Actor { * @ignore */ async callTask(taskId: string, input?: Dictionary, options: CallTaskOptions = {}): Promise { - const timeout = options.timeout === 'inherit' ? this.getRemainingTime() : options.timeout; + const timeout = options.timeout === 'inherit' ? this.getRemainingTimeSecs() : options.timeout; const { token, ...rest } = options; const client = token ? this.newClient({ token }) : this.apifyClient; @@ -2309,7 +2309,7 @@ export class Actor { const env = this.getEnv(); const MINIMUM_API_TIMEOUT_SECS = 1; if (this.isAtHome() && env.timeoutAt !== null) { - return Math.max(Math.ceil((env.timeoutAt.getTime() - Date.now()) / 1000), smallestPossibleApiTimeout); + return Math.max(Math.ceil((env.timeoutAt.getTime() - Date.now()) / 1000), MINIMUM_API_TIMEOUT_SECS); } log.warning( 'Using `inherit` argument is only possible when the Actor is running on the Apify platform and when the ' +