Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
392dcea
feat: redesign Configuration class for v4
B4nan Mar 13, 2026
18a23d9
fix: update configuration integration for crawlee Configuration redesign
B4nan Mar 25, 2026
4f7f6f9
fix: preserve storageClientOptions pass-through in Actor.newClient
B4nan Apr 20, 2026
b6c34ea
chore: bump crawlee to ^4.0.0-beta.49 and finish config-redesign inte…
B4nan Apr 30, 2026
9d11039
test(actor): reset service locator and Actor singleton between newCli…
B4nan Apr 30, 2026
be6811f
test: also reset SDK Configuration.globalConfig between cases
B4nan Apr 30, 2026
58ce536
chore: silence no-underscore-dangle for Actor._instance reset
B4nan Apr 30, 2026
5a34b34
chore: prettier
B4nan Apr 30, 2026
b42b603
test(actor): adapt to crawlee v4 eager Configuration resolution
B4nan Apr 30, 2026
2d90549
test(getInput): reset cached singletons before each fresh Actor build
B4nan Apr 30, 2026
41e6689
test(getInput): also overwrite actor.config explicitly for env-var re…
B4nan Apr 30, 2026
09fdf11
test(actor): import SDK Configuration in test file (not crawlee's)
B4nan Apr 30, 2026
5b6598a
chore: add cheerio as devDep — workaround missing @crawlee/linkedom dep
B4nan Apr 30, 2026
2711b95
chore: bump crawlee to ^4.0.0-beta.51 and drop cheerio workaround
B4nan Apr 30, 2026
a39ed7c
refactor(config): address PR review comments
B4nan May 11, 2026
e49ce5f
chore: prettier — fix formatting in proxy_configuration.ts
B4nan May 11, 2026
bda89cc
fix: preserve storageClientOptions pass-through in Actor.newClient
B4nan May 11, 2026
7970267
refactor: drop redundant `as Configuration` casts on this.config
B4nan May 11, 2026
691c7ce
fix: keep `|| Infinity` for maxTotalChargeUsd so `0` means "no limit"
B4nan May 12, 2026
28eab9a
fix(config): default isAtHome to false; resolve maxTotalChargeUsd via…
B4nan May 12, 2026
2d32490
fix(config): translate explicit `0` for maxTotalChargeUsd to Infinity…
B4nan May 12, 2026
b1f74e7
feat: add Configuration.reset() / Actor.reset(); use them in tests
B4nan May 12, 2026
0574338
refactor: rename Actor.reset() → Actor.resetGlobalState(), mark @inte…
B4nan May 12, 2026
8070d32
chore: bump @crawlee/* to ^4.0.0-beta.56, use super.reset() in Config…
B4nan May 12, 2026
24ae9b4
refactor(tests): move resetGlobalState() to a test helper
B4nan May 13, 2026
ee7835d
feat(actor): accept a pre-built Configuration via `new Actor({ config…
B4nan May 14, 2026
a00a8f7
fix(actor): honour `this.config` instead of `Configuration.getGlobalC…
B4nan May 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,715 changes: 908 additions & 807 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
"@apify/input_secrets": "^1.1.72",
"@apify/tsconfig": "^0.1.1",
"@commitlint/config-conventional": "^19.8.1",
"@crawlee/core": "^4.0.0-beta.56",
"@crawlee/types": "^4.0.0-beta.56",
"@crawlee/utils": "^4.0.0-beta.56",
"@playwright/browser-chromium": "^1.52.0",
"@types/content-type": "^1.1.8",
"@types/fs-extra": "^11.0.4",
Expand All @@ -78,10 +81,7 @@
"@types/tough-cookie": "^4.0.5",
"@types/ws": "^8.18.1",
"commitlint": "^19.8.1",
"crawlee": "^4.0.0-beta.0",
"@crawlee/core": "^4.0.0-beta.0",
"@crawlee/types": "^4.0.0-beta.0",
"@crawlee/utils": "^4.0.0-beta.0",
"crawlee": "^4.0.0-beta.56",
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.5",
"fs-extra": "^11.3.0",
Expand Down
9 changes: 5 additions & 4 deletions packages/apify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,16 @@
"@apify/log": "^2.5.18",
"@apify/timeout": "^0.3.2",
"@apify/utilities": "^2.15.5",
"@crawlee/core": "^4.0.0-beta.0",
"@crawlee/types": "^4.0.0-beta.0",
"@crawlee/utils": "^4.0.0-beta.0",
"@crawlee/core": "^4.0.0-beta.56",
"@crawlee/types": "^4.0.0-beta.56",
"@crawlee/utils": "^4.0.0-beta.56",
"apify-client": "^2.12.4",
"fs-extra": "^11.3.0",
"got-scraping": "^4.1.1",
"ow": "^2.0.0",
"semver": "^7.7.2",
"tslib": "^2.8.1",
"ws": "^8.18.2"
"ws": "^8.18.2",
"zod": "^3.24.0 || ^4.0.0"
}
}
113 changes: 63 additions & 50 deletions packages/apify/src/actor.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { createPrivateKey } from 'node:crypto';

import type {
ConfigurationOptions,
EventManager,
EventTypeName,
IStorage,
RecordOptions,
UseStateOptions,
} from '@crawlee/core';
import {
Configuration as CoreConfiguration,
Dataset,
EventType,
purgeDefaultStorages,
RequestQueue,
serviceLocator,
StorageManager,
} from '@crawlee/core';
import type {
Expand Down Expand Up @@ -46,6 +45,7 @@ import { addTimeoutToPromise } from '@apify/timeout';

import type { ChargeOptions, ChargeResult } from './charging.js';
import { ChargingManager } from './charging.js';
import type { ConfigurationOptions } from './configuration.js';
import { Configuration } from './configuration.js';
import { KeyValueStore } from './key_value_store.js';
import { PlatformEventManager } from './platform_event_manager.js';
Expand All @@ -57,6 +57,21 @@ import {
printOutdatedSdkWarning,
} from './utils.js';

/**
* Options accepted by the {@link Actor} constructor. Either pass field-level
* overrides (`token`, `inputKey`, …) — which the Actor will turn into a fresh
* {@link Configuration} — or pass a pre-built `configuration` instance. When
* both are present, `configuration` wins and the field-level overrides are
* ignored.
*/
export type ActorOptions = ConfigurationOptions & {
/**
* Pre-built {@link Configuration} instance the Actor should use. Takes
* precedence over any field-level overrides also passed in `options`.
*/
configuration?: Configuration;
};

export interface InitOptions {
storage?: StorageClient;
}
Expand Down Expand Up @@ -363,12 +378,17 @@ export class Actor<Data extends Dictionary = Dictionary> {

private chargingManager: ChargingManager;

constructor(options: ConfigurationOptions = {}) {
// use default configuration object if nothing overridden (it fallbacks to env vars)
this.config =
Object.keys(options).length === 0
? Configuration.getGlobalConfig()
: new Configuration(options);
constructor(options: ActorOptions = {}) {
const { configuration, ...configOptions } = options;
if (configuration) {
// BYO Configuration takes precedence; field-level overrides are
// ignored to keep the contract unambiguous.
this.config = configuration;
} else if (Object.keys(configOptions).length === 0) {
this.config = Configuration.getGlobalConfig();
} else {
this.config = new Configuration(configOptions);
}
this.apifyClient = this.newClient();
this.eventManager = new PlatformEventManager(this.config);
this.chargingManager = new ChargingManager(
Expand Down Expand Up @@ -489,20 +509,23 @@ export class Actor<Data extends Dictionary = Dictionary> {
log.info('System info', getSystemInfo());
printOutdatedSdkWarning();

// reset global config instance to respect APIFY_ prefixed env vars
CoreConfiguration.globalConfig = Configuration.getGlobalConfig();
// Register this Actor's Configuration with the service locator so
// crawlee internals (event manager, storage client, …) created later
// see the same instance. Honours a custom `configuration` passed to
// the Actor constructor instead of falling back to the global.
serviceLocator.setConfiguration(this.config);

if (this.isAtHome()) {
this.config.set('availableMemoryRatio', 1);
this.config.set('disableBrowserSandbox', true); // for browser launcher, adds `--no-sandbox` to args
this.config.useStorageClient(this.apifyClient);
this.config.useEventManager(this.eventManager);
// availableMemoryRatio and disableBrowserSandbox are now set via
// conditional defaults in the Configuration constructor (isAtHome check)
serviceLocator.setStorageClient(this.apifyClient);
serviceLocator.setEventManager(this.eventManager);
} else if (options.storage) {
this.config.useStorageClient(options.storage);
serviceLocator.setStorageClient(options.storage);
}

// Init the event manager the config uses
await this.config.getEventManager().init();
await serviceLocator.getEventManager().init();
log.debug(`Events initialized`);

await purgeDefaultStorages({
Expand Down Expand Up @@ -534,8 +557,8 @@ export class Actor<Data extends Dictionary = Dictionary> {
options.exit ??= true;
options.exitCode ??= EXIT_CODES.SUCCESS;
options.timeoutSecs ??= 30;
const client = this.config.getStorageClient();
const events = this.config.getEventManager();
const client = serviceLocator.getStorageClient();
const events = serviceLocator.getEventManager();

// Close the event manager and emit the final PERSIST_STATE event
await events.close();
Expand Down Expand Up @@ -601,14 +624,14 @@ export class Actor<Data extends Dictionary = Dictionary> {
* @ignore
*/
on(event: EventTypeName, listener: (...args: any[]) => any): void {
this.config.getEventManager().on(event, listener);
serviceLocator.getEventManager().on(event, listener);
}

/**
* @ignore
*/
off(event: EventTypeName, listener?: (...args: any[]) => any): void {
this.config.getEventManager().off(event, listener);
serviceLocator.getEventManager().off(event, listener);
}

/**
Expand Down Expand Up @@ -776,12 +799,10 @@ export class Actor<Data extends Dictionary = Dictionary> {
}

const {
customAfterSleepMillis = this.config.get(
'metamorphAfterSleepMillis',
),
customAfterSleepMillis = this.config.metamorphAfterSleepMillis,
...metamorphOpts
} = options;
const runId = this.config.get('actorRunId')!;
const runId = this.config.actorRunId!;
await this.apifyClient
.run(runId)
.metamorph(targetActorId, input, metamorphOpts);
Expand Down Expand Up @@ -815,27 +836,24 @@ export class Actor<Data extends Dictionary = Dictionary> {
this.isRebooting = true;

// Waiting for all the listeners to finish, as `.reboot()` kills the container.
const eventManager = serviceLocator.getEventManager();
await Promise.all([
// `persistState` for individual RequestLists, RequestQueue... instances to be persisted
...this.config
.getEventManager()
...eventManager
.listeners(EventType.PERSIST_STATE)
.map(async (x) => x()),
.map(async (x: (...args: any[]) => any) => x()),
// `migrating` to pause Apify crawlers
...this.config
.getEventManager()
...eventManager
.listeners(EventType.MIGRATING)
.map(async (x) => x()),
.map(async (x: (...args: any[]) => any) => x()),
]);

const runId = this.config.get('actorRunId')!;
const runId = this.config.actorRunId!;
await this.apifyClient.run(runId).reboot();

// Wait some time for container to be stopped.
const {
customAfterSleepMillis = this.config.get(
'metamorphAfterSleepMillis',
),
customAfterSleepMillis = this.config.metamorphAfterSleepMillis,
} = options;
await sleep(customAfterSleepMillis);
}
Expand Down Expand Up @@ -873,7 +891,7 @@ export class Actor<Data extends Dictionary = Dictionary> {
return undefined;
}

const runId = this.config.get('actorRunId')!;
const runId = this.config.actorRunId!;
if (!runId) {
throw new Error(
`Environment variable ${ACTOR_ENV_VARS.RUN_ID} is not set!`,
Expand Down Expand Up @@ -924,7 +942,7 @@ export class Actor<Data extends Dictionary = Dictionary> {
break;
}

const client = this.config.getStorageClient();
const client = serviceLocator.getStorageClient();

// just to be sure, this should be fast
await addTimeoutToPromise(
Expand All @@ -937,7 +955,7 @@ export class Actor<Data extends Dictionary = Dictionary> {
'Setting status message timed out after 1s',
).catch((e) => log.warning(e.message));

const runId = this.config.get('actorRunId')!;
const runId = this.config.actorRunId!;

if (runId) {
// just to be sure, this should be fast
Expand Down Expand Up @@ -1213,13 +1231,9 @@ export class Actor<Data extends Dictionary = Dictionary> {
async getInput<T = Dictionary | string | Buffer>(): Promise<T | null> {
this._ensureActorInit('getInput');

const inputSecretsPrivateKeyFile = this.config.get(
'inputSecretsPrivateKeyFile',
);
const inputSecretsPrivateKeyPassphrase = this.config.get(
'inputSecretsPrivateKeyPassphrase',
);
const input = await this.getValue<T>(this.config.get('inputKey'));
const { inputSecretsPrivateKeyFile } = this.config;
const { inputSecretsPrivateKeyPassphrase } = this.config;
const input = await this.getValue<T>(this.config.inputKey);
if (
ow.isValid(input, ow.object.nonEmpty) &&
inputSecretsPrivateKeyFile &&
Expand Down Expand Up @@ -1476,13 +1490,12 @@ export class Actor<Data extends Dictionary = Dictionary> {
* @ignore
*/
newClient(options: ApifyClientOptions = {}): ApifyClient {
const { storageDir, ...storageClientOptions } = this.config.get(
'storageClientOptions',
) as Dictionary;
const { storageDir, ...storageClientOptions } = (this.config
.storageClientOptions ?? {}) as Dictionary;
const { apifyVersion, crawleeVersion } = getSystemInfo();
return new ApifyClient({
baseUrl: this.config.get('apiBaseUrl'),
token: this.config.get('token'),
baseUrl: this.config.apiBaseUrl,
token: this.config.token,
userAgentSuffix: [
`SDK/${apifyVersion}`,
`Crawlee/${crawleeVersion}`,
Expand Down Expand Up @@ -1515,7 +1528,7 @@ export class Actor<Data extends Dictionary = Dictionary> {
options?: UseStateOptions,
) {
const kvStore = await KeyValueStore.open(options?.keyValueStoreName, {
config: options?.config || Configuration.getGlobalConfig(),
config: options?.config || this.config,
});
return kvStore.getAutoSavedValue<State>(
name || 'APIFY_GLOBAL_STATE',
Expand Down
13 changes: 6 additions & 7 deletions packages/apify/src/charging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,20 +87,19 @@ export class ChargingManager {
private apifyClient: ApifyClient;

constructor(configuration: Configuration, apifyClient: ApifyClient) {
this.maxTotalChargeUsd =
configuration.get('maxTotalChargeUsd') || Infinity; // convert `0` to `Infinity` in case the value is an empty string
this.isAtHome = configuration.get('isAtHome');
this.actorRunId = configuration.get('actorRunId');
this.purgeChargingLogDataset = configuration.get('purgeOnStart');
this.useChargingLogDataset = configuration.get('useChargingLogDataset');
this.maxTotalChargeUsd = configuration.maxTotalChargeUsd;
this.isAtHome = configuration.isAtHome;
this.actorRunId = configuration.actorRunId;
this.purgeChargingLogDataset = configuration.purgeOnStart;
this.useChargingLogDataset = configuration.useChargingLogDataset;

if (this.useChargingLogDataset && this.isAtHome) {
throw new Error(
'Using the ACTOR_USE_CHARGING_LOG_DATASET environment variable is only supported in a local development environment',
);
}

if (configuration.get('testPayPerEvent')) {
if (configuration.testPayPerEvent) {
if (this.isAtHome) {
throw new Error(
'Using the ACTOR_TEST_PAY_PER_EVENT environment variable is only supported in a local development environment',
Expand Down
Loading
Loading