@crawlee/core 4.0.0-beta.166 → 4.0.0-beta.168
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.
- package/crawlers/crawler_commons.d.ts +41 -1
- package/errors.d.ts +10 -0
- package/errors.js +12 -0
- package/package.json +6 -6
- package/storages/transaction.d.ts +11 -3
- package/storages/transaction.js +41 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Dictionary, HttpRequestOptions, ISession, ProxyInfo, SendRequestOptions } from '@crawlee/types';
|
|
1
|
+
import type { Awaitable, Dictionary, HttpRequestOptions, ISession, ProxyInfo, SendRequestOptions } from '@crawlee/types';
|
|
2
2
|
import type { ReadonlyDeep } from 'type-fest';
|
|
3
3
|
import type { EnqueueUrlsOptions } from '../enqueue_links/enqueue_links.js';
|
|
4
4
|
import type { CrawleeLogger } from '../log.js';
|
|
@@ -136,6 +136,46 @@ export interface CrawlingContext<UserData extends Dictionary = Dictionary> exten
|
|
|
136
136
|
* here can land more than once for a single request. Push results from the request handler itself.
|
|
137
137
|
*/
|
|
138
138
|
registerDeferredCleanup(cleanup: () => Promise<unknown>): void;
|
|
139
|
+
/**
|
|
140
|
+
* Registers `callback` to run once the request's storage writes have been committed, or once
|
|
141
|
+
* committing them has failed - the point where a write made in the handler can finally be reacted
|
|
142
|
+
* to where it was made. Two things belong here: side effects that must agree with what actually
|
|
143
|
+
* landed (result counters, progress reporting), and handling of a write that did not land at all.
|
|
144
|
+
*
|
|
145
|
+
* ```ts
|
|
146
|
+
* async requestHandler({ pushData, afterStorageCommit, useState }) {
|
|
147
|
+
* const state = await useState({ itemCount: 0 });
|
|
148
|
+
*
|
|
149
|
+
* await pushData(item);
|
|
150
|
+
* afterStorageCommit((error) => {
|
|
151
|
+
* if (error) {
|
|
152
|
+
* throw new NonRetryableError('The page is too big to store', { cause: error });
|
|
153
|
+
* }
|
|
154
|
+
* state.itemCount++;
|
|
155
|
+
* });
|
|
156
|
+
* }
|
|
157
|
+
* ```
|
|
158
|
+
*
|
|
159
|
+
* `useState()` is deliberately not transactional, which is what makes the counter case necessary:
|
|
160
|
+
* incrementing in the handler would keep the increment for a request that pushes an item and then
|
|
161
|
+
* fails, while the item itself is rolled back.
|
|
162
|
+
*
|
|
163
|
+
* Callbacks run in registration order, after the commit and before the request is marked as handled.
|
|
164
|
+
* The first one to throw propagates and the rest do not run; on a failed commit its error replaces
|
|
165
|
+
* the commit error, so a callback can decide what the request fails with. After a *successful*
|
|
166
|
+
* commit, a callback that throws fails the request **without a retry** whatever it throws - the
|
|
167
|
+
* writes are already durable and retrying would duplicate them, so anything that is not already a
|
|
168
|
+
* {@link NonRetryableError} is wrapped in an {@link AfterCommitError}.
|
|
169
|
+
*
|
|
170
|
+
* Callbacks are *not* run when the request handler itself fails - nothing was written, and
|
|
171
|
+
* `errorHandler` / `failedRequestHandler` cover that.
|
|
172
|
+
*
|
|
173
|
+
* Throws when storage is not transactional (`transactionalStorage: false`) - writes are then applied
|
|
174
|
+
* as they are made and throw at the call site on their own. In {@link AdaptivePlaywrightCrawler}
|
|
175
|
+
* the callback belongs to the current request handler attempt, so it runs only for the attempt whose
|
|
176
|
+
* writes are the ones being committed.
|
|
177
|
+
*/
|
|
178
|
+
afterStorageCommit(callback: (error?: Error) => Awaitable<void>): void;
|
|
139
179
|
/**
|
|
140
180
|
* Gives the current request `secs` more seconds to finish, for when how long it needs is only apparent
|
|
141
181
|
* once it is already running - a listing page that turns out to have far more to scroll through than
|
package/errors.d.ts
CHANGED
|
@@ -99,6 +99,16 @@ export declare class ContextPipelineCleanupError extends CriticalError {
|
|
|
99
99
|
export declare class RequestHandlerError extends Error {
|
|
100
100
|
constructor(error: unknown, options?: ErrorOptions);
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Wraps the failure of a callback registered with {@link StorageTransaction.afterCommit|`afterCommit`}
|
|
104
|
+
* that ran after the request's writes had already been committed.
|
|
105
|
+
*
|
|
106
|
+
* Non-retryable by nature: the writes are durable, so re-running the request handler would duplicate
|
|
107
|
+
* them. A callback that throws a `NonRetryableError` of its own is left alone.
|
|
108
|
+
*/
|
|
109
|
+
export declare class AfterCommitError extends NonRetryableError {
|
|
110
|
+
constructor(cause: unknown);
|
|
111
|
+
}
|
|
102
112
|
/**
|
|
103
113
|
* Thrown when attempting to set a different service instance after one has already been retrieved.
|
|
104
114
|
*/
|
package/errors.js
CHANGED
|
@@ -124,6 +124,18 @@ export class RequestHandlerError extends Error {
|
|
|
124
124
|
super(undefined, { cause: error, ...options });
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Wraps the failure of a callback registered with {@link StorageTransaction.afterCommit|`afterCommit`}
|
|
129
|
+
* that ran after the request's writes had already been committed.
|
|
130
|
+
*
|
|
131
|
+
* Non-retryable by nature: the writes are durable, so re-running the request handler would duplicate
|
|
132
|
+
* them. A callback that throws a `NonRetryableError` of its own is left alone.
|
|
133
|
+
*/
|
|
134
|
+
export class AfterCommitError extends NonRetryableError {
|
|
135
|
+
constructor(cause) {
|
|
136
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
127
139
|
/**
|
|
128
140
|
* Thrown when attempting to set a different service instance after one has already been retrieved.
|
|
129
141
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crawlee/core",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.168",
|
|
4
4
|
"description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.0.0"
|
|
@@ -52,10 +52,10 @@
|
|
|
52
52
|
"@apify/log": "^3.0.1",
|
|
53
53
|
"@apify/timeout": "^1.0.1",
|
|
54
54
|
"@apify/utilities": "^3.0.1",
|
|
55
|
-
"@crawlee/fs-storage": "4.0.0-beta.
|
|
56
|
-
"@crawlee/http-client": "4.0.0-beta.
|
|
57
|
-
"@crawlee/types": "4.0.0-beta.
|
|
58
|
-
"@crawlee/utils": "4.0.0-beta.
|
|
55
|
+
"@crawlee/fs-storage": "4.0.0-beta.168",
|
|
56
|
+
"@crawlee/http-client": "4.0.0-beta.168",
|
|
57
|
+
"@crawlee/types": "4.0.0-beta.168",
|
|
58
|
+
"@crawlee/utils": "4.0.0-beta.168",
|
|
59
59
|
"@sapphire/async-queue": "^1.5.5",
|
|
60
60
|
"@standard-schema/spec": "^1.0.0",
|
|
61
61
|
"@vladfrangu/async_event_emitter": "^2.4.6",
|
|
@@ -78,5 +78,5 @@
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
},
|
|
81
|
-
"gitHead": "
|
|
81
|
+
"gitHead": "fc75a7073fc330331ff2d68d81a8e6815ab4edf2"
|
|
82
82
|
}
|
|
@@ -162,6 +162,13 @@ export declare class StorageTransaction implements StorageTransactionView {
|
|
|
162
162
|
* @internal
|
|
163
163
|
*/
|
|
164
164
|
recordJournalEntry(entry: JournalEntry): void;
|
|
165
|
+
/**
|
|
166
|
+
* Registers a callback to run once a commit has been attempted, whether it succeeded (`error` is
|
|
167
|
+
* `undefined`) or failed - the only point at which a deferred write can be reacted to where it was
|
|
168
|
+
* made. Callbacks run in registration order; the first to throw propagates, the rest do not run, and
|
|
169
|
+
* on a failed commit its error replaces the commit error. Not run on rollback: nothing was written.
|
|
170
|
+
*/
|
|
171
|
+
afterCommit(callback: (error?: Error) => Awaitable<void>): void;
|
|
165
172
|
/**
|
|
166
173
|
* Replays the journaled writes into real storage. A no-op unless the transaction is `open`.
|
|
167
174
|
*
|
|
@@ -179,9 +186,10 @@ export declare class StorageTransaction implements StorageTransactionView {
|
|
|
179
186
|
*/
|
|
180
187
|
rollback(): void;
|
|
181
188
|
/**
|
|
182
|
-
* Releases the journal
|
|
183
|
-
* state, `failed` included. Idempotent, never throws, and does not
|
|
184
|
-
* {@link StorageTransactionView} of this transaction is only valid until this
|
|
189
|
+
* Releases the journal, the write-time snapshots it holds and any registered commit callbacks. Must
|
|
190
|
+
* be called for *every* terminal state, `failed` included. Idempotent, never throws, and does not
|
|
191
|
+
* change `state`. Any {@link StorageTransactionView} of this transaction is only valid until this
|
|
192
|
+
* is called.
|
|
185
193
|
*/
|
|
186
194
|
dispose(): void;
|
|
187
195
|
get datasetItems(): {
|
package/storages/transaction.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import { addTimeoutToPromise, storage as timeoutStorage, tryCancel } from '@apify/timeout';
|
|
3
3
|
import { serviceLocator } from '../service_locator.js';
|
|
4
|
+
import { AfterCommitError, NonRetryableError } from '../errors.js';
|
|
4
5
|
const DEFAULT_STORAGE_WRITE_POLICY = { requestQueue: 'writeThrough' };
|
|
5
6
|
const DEFAULT_COMMIT_TIMEOUT_MILLIS = 300_000;
|
|
6
7
|
const transactionStorage = new AsyncLocalStorage();
|
|
@@ -24,6 +25,8 @@ export class StorageTransaction {
|
|
|
24
25
|
#commitTimeoutMillis;
|
|
25
26
|
#state = 'open';
|
|
26
27
|
#disposed = false;
|
|
28
|
+
/** Callbacks registered via `afterCommit()`, run in registration order once a commit has been attempted. */
|
|
29
|
+
#afterCommitCallbacks = [];
|
|
27
30
|
/** @internal */
|
|
28
31
|
constructor(options = {}) {
|
|
29
32
|
this.policy = { ...DEFAULT_STORAGE_WRITE_POLICY, ...options.policy };
|
|
@@ -53,6 +56,18 @@ export class StorageTransaction {
|
|
|
53
56
|
}
|
|
54
57
|
this.journal.push(entry);
|
|
55
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Registers a callback to run once a commit has been attempted, whether it succeeded (`error` is
|
|
61
|
+
* `undefined`) or failed - the only point at which a deferred write can be reacted to where it was
|
|
62
|
+
* made. Callbacks run in registration order; the first to throw propagates, the rest do not run, and
|
|
63
|
+
* on a failed commit its error replaces the commit error. Not run on rollback: nothing was written.
|
|
64
|
+
*/
|
|
65
|
+
afterCommit(callback) {
|
|
66
|
+
if (!this.isActive) {
|
|
67
|
+
throw new Error(`Cannot register a commit callback on a transaction in the '${this.#state}' state`);
|
|
68
|
+
}
|
|
69
|
+
this.#afterCommitCallbacks.push(callback);
|
|
70
|
+
}
|
|
56
71
|
/**
|
|
57
72
|
* Replays the journaled writes into real storage. A no-op unless the transaction is `open`.
|
|
58
73
|
*
|
|
@@ -72,12 +87,31 @@ export class StorageTransaction {
|
|
|
72
87
|
// frames, so a request-handler timeout that already fired would abort the commit of a
|
|
73
88
|
// handler that succeeded. Hence a fresh timeout context, which also provides the time bound.
|
|
74
89
|
await timeoutStorage.exit(async () => addTimeoutToPromise(async () => this.flush(), this.#commitTimeoutMillis, `Committing the storage transaction timed out after ${this.#commitTimeoutMillis / 1000} seconds.`));
|
|
75
|
-
this.#state = 'committed';
|
|
76
90
|
}
|
|
77
91
|
catch (error) {
|
|
78
92
|
this.#state = 'failed';
|
|
79
|
-
|
|
93
|
+
// Callbacks are handed an `Error`, so a backend that rejects with anything else gets wrapped.
|
|
94
|
+
const commitError = error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
95
|
+
// A callback that throws here propagates instead: it was handed the failure and decided what
|
|
96
|
+
// the request should fail with.
|
|
97
|
+
await this.#runAfterCommitCallbacks(commitError);
|
|
98
|
+
throw commitError;
|
|
80
99
|
}
|
|
100
|
+
this.#state = 'committed';
|
|
101
|
+
try {
|
|
102
|
+
await this.#runAfterCommitCallbacks();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throw error instanceof NonRetryableError ? error : new AfterCommitError(error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Fresh timeout context for the same reason as the flush. */
|
|
109
|
+
async #runAfterCommitCallbacks(error) {
|
|
110
|
+
await timeoutStorage.exit(async () => {
|
|
111
|
+
for (const callback of this.#afterCommitCallbacks) {
|
|
112
|
+
await callback(error);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
81
115
|
}
|
|
82
116
|
async flush() {
|
|
83
117
|
// Each participating frontend replays all of its buffered entries in one call. Frontends are
|
|
@@ -112,9 +146,10 @@ export class StorageTransaction {
|
|
|
112
146
|
this.#state = 'rolledBack';
|
|
113
147
|
}
|
|
114
148
|
/**
|
|
115
|
-
* Releases the journal
|
|
116
|
-
* state, `failed` included. Idempotent, never throws, and does not
|
|
117
|
-
* {@link StorageTransactionView} of this transaction is only valid until this
|
|
149
|
+
* Releases the journal, the write-time snapshots it holds and any registered commit callbacks. Must
|
|
150
|
+
* be called for *every* terminal state, `failed` included. Idempotent, never throws, and does not
|
|
151
|
+
* change `state`. Any {@link StorageTransactionView} of this transaction is only valid until this
|
|
152
|
+
* is called.
|
|
118
153
|
*/
|
|
119
154
|
dispose() {
|
|
120
155
|
if (this.#disposed) {
|
|
@@ -134,6 +169,7 @@ export class StorageTransaction {
|
|
|
134
169
|
}
|
|
135
170
|
this.#disposed = true;
|
|
136
171
|
this.journal.length = 0;
|
|
172
|
+
this.#afterCommitCallbacks.length = 0;
|
|
137
173
|
}
|
|
138
174
|
get datasetItems() {
|
|
139
175
|
return this.journal.flatMap((entry) => entry.type === 'dataset' ? entry.items.map((item) => ({ item, datasetId: entry.storageId })) : []);
|