create static method

Future<SDKServices> create({
  1. required DigiaConfig config,
  2. required LocalStorage storage,
  3. required NetworkClientFactory networkClient,
  4. required void onLiveTestCampaign(
    1. LiveTestInvocation invocation
    ),
  5. required AnalyticsQueue analyticsQueue,
  6. Map<String, dynamic>? staticContext,
  7. int clock()?,
})

The composition root: builds every service from config and storage. Async only to read the static device/app context once, up front, so no service is ever observed half-built.

storage must be the warmed root storage, after LocalStorageMigrator has run. This is the only place storage is scoped per domain. The process singletons the SDK still has (component registry, live test, debug overlay) get their storage and client here too.

analyticsQueue is passed in, not built: it owns a database file and must outlive this container across re-initialization. dispose leaves it open.

Implementation

static Future<SDKServices> create({
  required DigiaConfig config,
  required LocalStorage storage,
  required NetworkClientFactory networkClient,
  required void Function(LiveTestInvocation invocation) onLiveTestCampaign,
  required AnalyticsQueue analyticsQueue,
  Map<String, dynamic>? staticContext,
  int Function()? clock,
}) async {
  if (staticContext == null) {
    try {
      staticContext = await DigiaAnalyticsService.buildStaticContext();
    } catch (error) {
      // Degrade, don't fail init: events and headers go out without it.
      _log.e('Static context unavailable — continuing', error: error);
      staticContext = const {};
    }
  }
  final Map<String, dynamic> resolvedStaticContext = staticContext;

  final systemDeviceId = await IdentityManager.systemDeviceId();
  final identityManager = IdentityManager.create(
    storage: storage.scoped('identity'),
    newDeviceId: () => systemDeviceId,
  );
  final sessionManager = SessionManager.create(
    storage: storage.scoped('session'),
    timeoutMs: config.analyticsConfig.sessionTimeoutMs,
    clock: clock,
  );
  final sessionLifecycleObserver = SessionLifecycleObserver(sessionManager);
  WidgetsBinding.instance.addObserver(sessionLifecycleObserver);

  final headers = EngageHeaders(
    config: config,
    identityManager: identityManager,
    sessionManager: sessionManager,
    staticContext: resolvedStaticContext,
  );
  final client = networkClient(headers.call);
  final baseAnalyticsConfig = config.analyticsConfig;
  final analyticsConfig =
      (DigiaEndpoints.isTestMode && !config.isAnalyticsConfigExplicit)
          ? DigiaAnalyticsConfig(
              enabled: baseAnalyticsConfig.enabled,
              flushBatchSize: 1,
              flushIntervalMs: 100,
              maxBatchSize: baseAnalyticsConfig.maxBatchSize,
              queueMaxEvents: baseAnalyticsConfig.queueMaxEvents,
              sessionTimeoutMs: baseAnalyticsConfig.sessionTimeoutMs,
            )
          : baseAnalyticsConfig;
  final analyticsService = DigiaAnalyticsService(
    config: analyticsConfig,
    identityManager: identityManager,
    sessionManager: sessionManager,
    queue: analyticsQueue,
    networkClient: client,
    networkMonitor: PlatformNetworkMonitor(),
    backgroundTime: defaultTargetPlatform == TargetPlatform.iOS
        ? PluginBackgroundTime()
        : const NoBackgroundTime(),
    staticContext: resolvedStaticContext,
  );

  final sessionReporter = SessionReporter(
    sessionId: () => sessionManager.sessionId,
    anonymousId: () => identityManager.deviceId,
    userId: () => identityManager.userId,
    context: () => resolvedStaticContext,
    networkClient: client,
    storage: storage.scoped('session'),
  );
  final sessionIdentityWiring = SessionIdentityWiring(
    identityManager: identityManager,
    sessionManager: sessionManager,
    sessionReporter: config.analyticsConfig.enabled ? sessionReporter : null,
  )..attach();

  DigiaDebugOverlayController.instance.configure(storage.scoped('debug'));
  final componentRegistry = ComponentRegistryService.instance
    ..networkClient = client
    ..configure(config, storage.scoped('registry'));
  final liveTestService = LiveTestService.instance
    ..configure(
      onCampaignTest: onLiveTestCampaign,
      storage: storage.scoped('live_test'),
      networkClient: client,
    );

  return SDKServices(
    storage: storage,
    identityManager: identityManager,
    sessionManager: sessionManager,
    networkClient: client,
    analyticsService: analyticsService,
    frequencyManager: FrequencyManager(
      storage.scoped('frequency'),
      () => sessionManager.sessionId,
    ),
    campaignStore: CampaignStore(),
    submissionReporter: SubmissionReporter(
      networkClient: client,
      sessionId: () => sessionManager.sessionId,
    ),
    sessionReporter: sessionReporter,
    sessionLifecycleObserver: sessionLifecycleObserver,
    captureSession: CaptureSession(networkClient: client),
    componentRegistry: componentRegistry,
    liveTestService: liveTestService,
    sessionIdentityWiring: sessionIdentityWiring,
    captureStorage: storage.scoped('capture'),
  );
}