diff --git a/packages/apify/src/actor.ts b/packages/apify/src/actor.ts index c82b545f09..7358cb4b9e 100644 --- a/packages/apify/src/actor.ts +++ b/packages/apify/src/actor.ts @@ -68,6 +68,17 @@ export class Actor { */ readonly eventManager: EventManager; + /** + * Whether the actor instance was initialized. This is set by calling {@apilink Actor.init}. + */ + initialized = false; + + /** + * Set if the actor called a method that requires the instance to be initialized, but did not do so. + * A call to `init` after this warning is emitted is considered an invalid state and will throw an error. + */ + private warnedAboutMissingInitCall = false; + 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); @@ -165,6 +176,20 @@ export class Actor { * @ignore */ async init(options: InitOptions = {}): Promise { + if (this.initialized) { + return; + } + + // If the warning about forgotten init call was emitted, we will not continue the init procedure. + if (this.warnedAboutMissingInitCall) { + throw new Error([ + 'Actor.init() was called after a method that would access a storage client was used.', + 'This in an invalid state. Please make sure to call Actor.init() before such methods are called.', + ].join('\n')); + } + + this.initialized = true; + logSystemInfo(); printOutdatedSdkWarning(); @@ -523,6 +548,8 @@ export class Actor { * @ignore */ async pushData(item: Data | Data[]): Promise { + this._ensureActorInit('pushData'); + const dataset = await this.openDataset(); return dataset.pushData(item); } @@ -551,6 +578,8 @@ export class Actor { forceCloud: ow.optional.boolean, })); + this._ensureActorInit('openDataset'); + return this._openStorage>(Dataset, datasetIdOrName, options); } @@ -583,6 +612,8 @@ export class Actor { * @ignore */ async getValue(key: string): Promise { + this._ensureActorInit('getValue'); + const store = await this.openKeyValueStore(); return store.getValue(key); } @@ -619,6 +650,8 @@ export class Actor { * @ignore */ async setValue(key: string, value: T | null, options: RecordOptions = {}): Promise { + this._ensureActorInit('setValue'); + const store = await this.openKeyValueStore(); return store.setValue(key, value, options); } @@ -653,6 +686,8 @@ export class Actor { * @ignore */ async getInput(): Promise { + this._ensureActorInit('getInput'); + const inputSecretsPrivateKeyFile = this.config.get('inputSecretsPrivateKeyFile'); const inputSecretsPrivateKeyPassphrase = this.config.get('inputSecretsPrivateKeyPassphrase'); const input = await this.getValue(this.config.get('inputKey')); @@ -687,6 +722,8 @@ export class Actor { forceCloud: ow.optional.boolean, })); + this._ensureActorInit('openKeyValueStore'); + return this._openStorage(KeyValueStore, storeIdOrName, options); } @@ -713,6 +750,8 @@ export class Actor { forceCloud: ow.optional.boolean, })); + this._ensureActorInit('openRequestQueue'); + return this._openStorage(RequestQueue, queueIdOrName, options); } @@ -1374,6 +1413,24 @@ export class Actor { const client = options.forceCloud ? this.apifyClient : undefined; return StorageManager.openStorage(storageClass, id, client, this.config); } + + private _ensureActorInit(methodCalled: string) { + // If we already warned the user once, don't do it again to prevent spam + if (this.warnedAboutMissingInitCall) { + return; + } + + if (this.initialized) { + return; + } + + this.warnedAboutMissingInitCall = true; + + log.warning([ + `Actor.${methodCalled}() was called but the actor instance was not initialized.`, + 'Did you forget to call Actor.init()?', + ].join('\n')); + } } export interface InitOptions {