apify-client 3.0.0-beta.54 → 3.0.0-beta.56

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.
@@ -10298,7 +10298,8 @@ export interface operations {
10298
10298
  version: string;
10299
10299
  /**
10300
10300
  * @description If `true` or `1`, the system will use a cache to speed up the build
10301
- * process. By default, cache is not used.
10301
+ * process. Pass `false` or `0` to build without the cache. By default,
10302
+ * cache is used.
10302
10303
  */
10303
10304
  useCache?: boolean;
10304
10305
  /**
@@ -81,7 +81,8 @@ export declare class StreamedLog {
81
81
  */
82
82
  start(): void;
83
83
  /**
84
- * Stop log redirection. Waits up to one second for lines already in flight, then aborts the log stream request.
84
+ * Stop log redirection. Waits up to one second for lines already in flight, then aborts the log stream request. If
85
+ * no stream has delivered anything yet, reads the whole log in one request instead.
85
86
  */
86
87
  stop(): Promise<void>;
87
88
  }
@@ -3,7 +3,15 @@ import { z } from 'zod';
3
3
  import log, { Logger, LogLevel } from '@apify/log';
4
4
  import { ResourceClient } from '../base/resource_client.js';
5
5
  import { timeoutOptionsShape } from '../timeouts.js';
6
- import { cast, catchNotFoundForResourceOrThrow, concatBytes, parseArgument } from '../utils.js';
6
+ import { cast, catchNotFoundForResourceOrThrow, concatBytes, parseArgument, sleep } from '../utils.js';
7
+ /**
8
+ * Pause before the first reopen of a log stream that ended before the run logged anything. Each further reopen doubles
9
+ * the pause up to `EMPTY_LOG_STREAM_MAX_RETRY_MILLIS`, which bounds the request rate while a run waits long to start,
10
+ * for example for free memory.
11
+ */
12
+ const EMPTY_LOG_STREAM_RETRY_MILLIS = 500;
13
+ /** Upper bound on the pause between reopens of an empty log stream. */
14
+ const EMPTY_LOG_STREAM_MAX_RETRY_MILLIS = 5_000;
7
15
  const logOptionsSchema = z.strictObject({ raw: z.boolean().optional(), ...timeoutOptionsShape });
8
16
  /** How long `StreamedLog.stop()` waits for lines already in flight before aborting the log stream request. */
9
17
  const STOP_GRACE_MILLIS = 1000;
@@ -144,6 +152,7 @@ export class StreamedLog {
144
152
  #streamingTask = null;
145
153
  #stopLogging = false;
146
154
  #stopController = new AbortController();
155
+ #wakeController = new AbortController();
147
156
  constructor(options) {
148
157
  const { toLog, logClient, fromStart = true, signal } = options;
149
158
  this.#destinationLog = toLog;
@@ -160,16 +169,19 @@ export class StreamedLog {
160
169
  }
161
170
  this.#stopLogging = false;
162
171
  this.#stopController = new AbortController();
172
+ this.#wakeController = new AbortController();
163
173
  this.#streamingTask = this.#streamLog();
164
174
  }
165
175
  /**
166
- * Stop log redirection. Waits up to one second for lines already in flight, then aborts the log stream request.
176
+ * Stop log redirection. Waits up to one second for lines already in flight, then aborts the log stream request. If
177
+ * no stream has delivered anything yet, reads the whole log in one request instead.
167
178
  */
168
179
  async stop() {
169
180
  if (!this.#streamingTask) {
170
181
  throw new Error('Streaming task is not active');
171
182
  }
172
183
  this.#stopLogging = true;
184
+ this.#wakeController.abort();
173
185
  const stopController = this.#stopController;
174
186
  const abortTimeout = setTimeout(() => stopController.abort(), STOP_GRACE_MILLIS);
175
187
  try {
@@ -193,11 +205,41 @@ export class StreamedLog {
193
205
  ? AbortSignal.any([this.#signal, this.#stopController.signal])
194
206
  : this.#stopController.signal;
195
207
  try {
196
- const logStream = await this.#logClient.stream({ raw: true, signal });
197
- if (!logStream) {
198
- return;
208
+ let lastChunkRemainder;
209
+ let retryMillis = EMPTY_LOG_STREAM_RETRY_MILLIS;
210
+ // The API serves the log of a run that has not logged anything yet as an empty stream that ends at once,
211
+ // so reopen it until the first bytes arrive. If stopped before any bytes arrive, fetch it in one request.
212
+ while (!lastChunkRemainder) {
213
+ if (this.#stopLogging) {
214
+ // `signal` is aborted once the stop grace period ends, which would cut this read short.
215
+ const logContent = await this.#logClient.get({ raw: true, signal: this.#signal });
216
+ lastChunkRemainder = await this.#logStreamChunks([new TextEncoder().encode(logContent ?? '')], signal);
217
+ break;
218
+ }
219
+ let logStream;
220
+ try {
221
+ logStream = await this.#logClient.stream({ raw: true, signal });
222
+ }
223
+ catch (err) {
224
+ // The stop grace period ended while the stream was connecting, so read the log in one request.
225
+ if (this.#stopLogging && !this.#signal?.aborted)
226
+ continue;
227
+ throw err;
228
+ }
229
+ if (!logStream) {
230
+ return;
231
+ }
232
+ // A stream opened during stop() would be cut after its first chunk, so read the log in one request.
233
+ if (this.#stopLogging) {
234
+ logStream.destroy();
235
+ continue;
236
+ }
237
+ lastChunkRemainder = await this.#logStreamChunks(logStream, signal);
238
+ if (!lastChunkRemainder) {
239
+ await sleep(retryMillis, AbortSignal.any([signal, this.#wakeController.signal]));
240
+ retryMillis = Math.min(retryMillis * 2, EMPTY_LOG_STREAM_MAX_RETRY_MILLIS);
241
+ }
199
242
  }
200
- const lastChunkRemainder = await this.#logStreamChunks(logStream, signal);
201
243
  // Process whatever is left when exiting. Maybe it is incomplete, maybe it is last log without EOL.
202
244
  const lastMessage = this.#decoder.decode(lastChunkRemainder).trim();
203
245
  if (lastMessage.length) {
@@ -210,13 +252,18 @@ export class StreamedLog {
210
252
  log.warning(`Log redirection stopped due to error`, err);
211
253
  }
212
254
  }
255
+ /**
256
+ * Redirect every complete message in the chunks and return the incomplete rest, or `undefined` when there were no
257
+ * chunks at all.
258
+ */
213
259
  async #logStreamChunks(logStream, signal) {
214
260
  // Chunk may be incomplete. Keep remainder for next chunk.
215
- let previousChunkRemainder = new Uint8Array();
261
+ let previousChunkRemainder;
216
262
  try {
217
263
  for await (const chunk of logStream) {
218
264
  // Handle possible leftover incomplete line from previous chunk.
219
265
  // Everything before last end of line is complete.
266
+ previousChunkRemainder ??= new Uint8Array();
220
267
  const chunkWithPreviousRemainder = new Uint8Array(previousChunkRemainder.length + chunk.length);
221
268
  chunkWithPreviousRemainder.set(previousChunkRemainder, 0);
222
269
  chunkWithPreviousRemainder.set(chunk, previousChunkRemainder.length);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apify-client",
3
- "version": "3.0.0-beta.54",
3
+ "version": "3.0.0-beta.56",
4
4
  "description": "Apify API client for JavaScript",
5
5
  "engines": {
6
6
  "node": ">=22.13.0"
@@ -59,7 +59,7 @@
59
59
  ],
60
60
  "apify": {
61
61
  "openapiSpec": {
62
- "version": "v2-2026-10-01T153946Z"
62
+ "version": "v2-2026-10-08T073338Z"
63
63
  }
64
64
  },
65
65
  "dependencies": {