Problem
ServiceLocator throws a ServiceConflictError whenever a service that is already registered is set again to a different instance:
setConfiguration(config)
setStorageClient(client)
setEventManager(manager)
The only way to clear a registered service is serviceLocator.reset(), which drops all services at once (configuration, storage client, event manager, logger, and the storage-instance-manager cache). There is no way to replace a single service.
The conflict guard usefully prevents accidental double-registration, but it also makes several legitimate replacement scenarios impossible without nuking everything.
Concrete impact (Apify SDK v4 migration)
-
Actor.init() cannot run when a storage client is already registered. On the platform, Actor.init() registers an ApifyStorageClient. If anything has already set a storage client — a test harness using @crawlee/memory-storage, a second Actor.init(), or a user who configured their own client before init — setStorageClient throws.
-
Tests can't swap the storage client. A common pattern is: let Actor.init() register the real client, then swap in an in-memory storage to assert against pushed data. Under v4 this is impossible — reset() would also discard the configuration and event manager that init() just established.
In crawlee v3 the equivalent (Configuration.useStorageClient) allowed replacement, so this is a behavioural regression for these flows.
Repro (sketch)
import { serviceLocator } from '@crawlee/core';
import { MemoryStorage } from '@crawlee/memory-storage';
serviceLocator.setStorageClient(new MemoryStorage());
serviceLocator.setStorageClient(new MemoryStorage());
// throws: Service StorageClient is already in use.
Possible solutions
- Last-write-wins — make
setX() overwrite an existing service (optionally with a log.warning on replacement). Simplest, but loses the accidental-double-set guard.
- Explicit override flag —
setStorageClient(client, { override: true }); keeps the guard by default, allows intentional replacement.
- Granular per-service reset — add
resetStorageClient() / resetEventManager() / resetConfiguration() so a caller can clear one service without dropping the rest.
- Allow replacement until first read — track whether a service has been consumed (e.g.
getStorageClient() called) and allow replacement until then, throwing only on replace-after-use.
Preference is (2) or (3): they keep the conflict guard for the common accidental case while making intentional replacement an explicit, first-class operation.
Happy to open a PR once we agree on the shape.
Problem
ServiceLocatorthrows aServiceConflictErrorwhenever a service that is already registered is set again to a different instance:setConfiguration(config)setStorageClient(client)setEventManager(manager)The only way to clear a registered service is
serviceLocator.reset(), which drops all services at once (configuration, storage client, event manager, logger, and the storage-instance-manager cache). There is no way to replace a single service.The conflict guard usefully prevents accidental double-registration, but it also makes several legitimate replacement scenarios impossible without nuking everything.
Concrete impact (Apify SDK v4 migration)
Actor.init()cannot run when a storage client is already registered. On the platform,Actor.init()registers anApifyStorageClient. If anything has already set a storage client — a test harness using@crawlee/memory-storage, a secondActor.init(), or a user who configured their own client before init —setStorageClientthrows.Tests can't swap the storage client. A common pattern is: let
Actor.init()register the real client, then swap in an in-memory storage to assert against pushed data. Under v4 this is impossible —reset()would also discard the configuration and event manager thatinit()just established.In crawlee v3 the equivalent (
Configuration.useStorageClient) allowed replacement, so this is a behavioural regression for these flows.Repro (sketch)
Possible solutions
setX()overwrite an existing service (optionally with alog.warningon replacement). Simplest, but loses the accidental-double-set guard.setStorageClient(client, { override: true }); keeps the guard by default, allows intentional replacement.resetStorageClient()/resetEventManager()/resetConfiguration()so a caller can clear one service without dropping the rest.getStorageClient()called) and allow replacement until then, throwing only on replace-after-use.Preference is (2) or (3): they keep the conflict guard for the common accidental case while making intentional replacement an explicit, first-class operation.
Happy to open a PR once we agree on the shape.