init method

Future<void> init({
  1. required String? layoutID,
})

Initializes the instance.

If a layoutID is specified, that layout will be downloaded from the server first, then the rest of the layouts will be streamed passively in the background.

If a layoutID is null, all layouts will be downloaded immediately and awaited.

Implementation

Future<void> init({required String? layoutID}) async {
  DebugLogger.instance.printFunction('init(layoutID: $layoutID)', name: name);

  assert(
    layoutID != null || config.preload || config.slug != null,
    'If [layoutID] is null, [config.preload] must be true. If both are not '
    'specified, then a slug must be provided in the config.',
  );

  final Stopwatch stopwatch = Stopwatch()..start();

  status = CStatus.loading(CLoadingState.initializedDataManagers);

  queuingDone = false;

  // Initialize all locally cached data.
  final cachedModel = localDataRepository.fetchPublishModel(
    source: config.publishSource,
  );

  _publishModel ??= cachedModel;

  if (_publishModel != null) {
    DebugLogger.instance.printInfo(
      'Publish model is cached locally. Emitting.',
      name: name,
    );
    await emitPublishModel();

    loadFontsFromPublishModel();
  }

  // A slug was specified. We need a layout FAST.
  if (config.slug case String slug) {
    DebugLogger.instance.printInfo(
      '[slug] Slug was specified [$slug]. Going through slug flow...',
      name: name,
    );

    final Stopwatch bundleStopWatch = Stopwatch()..start();
    try {
      DebugLogger.instance.printInfo(
        '[slug] Downloading complete publish bundle for slug $slug.',
        name: name,
      );

      if (_publishModel == null) {
        DebugLogger.instance.printInfo(
          '[slug] Publish model is not cached locally. Downloading complete publish bundle for slug $slug in foreground.',
          name: name,
        );
      } else {
        DebugLogger.instance.printInfo(
          '[slug] Publish model is already cached locally. Downloading complete publish bundle for slug $slug in background.',
          name: name,
        );
      }

      final publishBundleFuture =
          fetchCompletePublishBundle(
            slug: slug,
            source: config.publishSource,
          ).then((success) {
            if (success) {
              DebugLogger.instance.printInfo(
                '[slug] Complete publish model from slug is downloaded in background. Emitting.',
                name: name,
              );

              loadFontsFromPublishModel();
            } else {
              errorLogger.captureException(
                'Failed to download complete publish bundle',
                message:
                    'Failed to download complete publish bundle for slug $slug.',
                type: 'bundle_download_failed',
              );
            }
          });

      if (_publishModel == null) {
        await publishBundleFuture;
        // Nothing cached and nothing downloaded: there is no app to show.
        // Fail, so the widget shows an error instead of loading forever.
        if (_publishModel == null) {
          throw Exception('Could not download the published app "$slug".');
        }
      } else {
        // Background refresh: report failures instead of leaving them as
        // unhandled async errors.
        unawaited(
          publishBundleFuture.catchError((Object e, StackTrace st) {
            errorLogger.captureException(
              e,
              message:
                  'Failed to refresh publish bundle for slug $slug in background.',
              type: 'bundle_download_failed',
              stackTrace: st,
            );
          }),
        );
      }

      _logTime(stopwatch);
    } catch (e, stackTrace) {
      DebugLogger.instance.log(
        '[slug] Failed to download complete publish bundle for slug $slug.\nError: $e',
        category: DebugCategory.error,
        name: name,
        level: Level.WARNING,
      );
      DebugLogger.instance.printInfo(
        '[slug] Since no publish model is cached, this is a complete stop to the data manager.',
        name: name,
      );

      _logTime(stopwatch);

      if (e is Exception) {
        ErrorLogger.instance.captureException(
          e,
          message:
              'Failed to download complete publish bundle for slug $slug.',
          type: 'bundle_download_failed',
          stackTrace: stackTrace,
        );
        rethrow;
      }
    } finally {
      bundleStopWatch.stop();
      DebugLogger.instance.printInfo(
        '[slug] Publish bundle flow took ${bundleStopWatch.elapsedMilliseconds}ms or ${bundleStopWatch.elapsed.inSeconds}s.',
        name: name,
      );
    }
  } else {
    DebugLogger.instance.printInfo(
      '[slug] Slug is ${config.slug}. Skipping slug flow.',
      name: name,
    );
  }

  if (authManager.authData == null) {
    DebugLogger.instance.printInfo(
      'No auth data is available. Continuing as if offline.',
      name: name,
    );
    _logTime(stopwatch);
    return;
  }

  final AuthData authData = authManager.authData!;

  // If a [layoutID] is not null, then that layout must be prioritized and
  // downloaded first if it is not already cached.
  //
  // If the publish model is still null, then that needs to be downloaded
  // first, and we do that by getting the first event of the stream below.
  //
  // Once a publish model is available in the future, if a [layoutID] is
  // specified, we try to download that layout then instead of here.
  final bool didPrepareLayout;
  if (_publishModel != null && layoutID != null) {
    if (!_publishModel!.layouts.containsKey(layoutID)) {
      // If the layout is disabled, we skip it.
      if (publishModel!.disabledLayouts.contains(layoutID)) {
        DebugLogger.instance.printInfo(
          'Layout [$layoutID] is marked as disabled. Skipping it during init.',
          name: name,
        );
        didPrepareLayout = false;
        _logTime(stopwatch);
      } else {
        DebugLogger.instance.printInfo(
          'Layout [$layoutID] during init is not cached locally. Downloading...',
          name: name,
        );

        try {
          didPrepareLayout = await getOrFetchPopulatedLayout(
            layoutID: layoutID,
          );
          DebugLogger.instance.printInfo(
            'Layout in init [$layoutID] fetch complete.',
            name: name,
          );
        } catch (e, str) {
          DebugLogger.instance.log(
            'Layout [$layoutID] failed to download during init.\nError: $e',
            category: DebugCategory.error,
            name: name,
            level: Level.WARNING,
          );
          ErrorLogger.instance.captureException(
            e,
            message: 'Failed to download layout [$layoutID] during init.',
            type: 'layout_download_failed',
            layoutID: layoutID,
            stackTrace: str,
          );

          _logTime(stopwatch);
          return;
        }
      }
    } else {
      DebugLogger.instance.printInfo(
        'Layout [$layoutID] during init is already cached locally. Skipping layout download.',
        name: name,
      );
      didPrepareLayout = true;
    }
  } else {
    if (_publishModel == null) {
      DebugLogger.instance.printInfo(
        'Publish model during init is not cached locally. Going to wait for the first publish model from the server.',
        name: name,
      );
    } else if (layoutID == null) {
      DebugLogger.instance.printInfo(
        'Publish model during init is available and layoutID is not specified. All layouts will be downloaded soon!',
        name: name,
      );
    } else {
      DebugLogger.instance.printInfo(
        'Publish model during init is available and layoutID is specified. Layout [$layoutID] will be downloaded soon from stream.',
        name: name,
      );
    }
    didPrepareLayout = false;
  }

  // Listen the publish model document.
  // It's either going to be fetched for the first time if it doesn't exist
  // in cache, or it's going to be updated with new data.
  final Future<SDKPublishModel> firstPublishEvent = listenToPublishModel(
    authData.projectId,
  );
  if (_publishModel != null) {
    // The cached model is used and the first event is not awaited. Its
    // errors are already reported by the stream listener.
    firstPublishEvent.ignore();
  }

  // If the publish model is still null, then we need to wait for the first
  // publish model to arrive from the server via the stream above.
  if (_publishModel == null) {
    DebugLogger.instance.printInfo(
      'Publish model is still null during init. Waiting for the first publish model from the server.',
      name: name,
    );
    final model = await firstPublishEvent;
    _publishModel = model;
    await emitPublishModel();
    savePublishModel();

    DebugLogger.instance.printInfo(
      'Publish model during init is now available. Proceeding with init!',
      name: name,
    );

    if (_publishModel == null) {
      DebugLogger.instance.printInfo(
        'Publish model is still null.\n'
        'Is there a network problem or bad authentication?',
        name: name,
      );
      _logTime(stopwatch);
      return;
    }
  }

  // If a [layoutID] was specified, then that layout must be prioritized and
  // downloaded first if it is not already cached.
  //
  // If we could not download it earlier, that would be because we did not
  // have a publish model available and needed to wait for one to arrive
  // from the server for the first time or that it failed to download for
  // some unknown reason.
  //
  // Perhaps the publish model was simply out of date locally,
  // but now that we fetched a new one, and [didPrepareLayout] is still
  // false, we can try to download the layout again with the new publish
  // model.
  //
  // At this stage of the function, we can be sure that a publish model
  // exists and can safely download the desired [layoutID], because if a
  // publish model is still null, we cannot proceed further and this
  // function terminates earlier.
  if (!didPrepareLayout && layoutID != null) {
    DebugLogger.instance.printInfo(
      'We can safely download layout [$layoutID] now.',
      name: name,
    );

    if (publishModel!.disabledLayouts.contains(layoutID)) {
      DebugLogger.instance.printInfo(
        'Layout [$layoutID] is disabled. Skipping it during init.',
        name: name,
      );
    }
    try {
      await getOrFetchPopulatedLayout(layoutID: layoutID);
      DebugLogger.instance.printInfo(
        'Layout [$layoutID] downloaded from init successfully.',
        name: name,
      );
    } catch (e, str) {
      ErrorLogger.instance.captureException(
        e,
        message: 'Failed to download layout [$layoutID] during init.',
        type: 'layout_download_failed',
        layoutID: layoutID,
        stackTrace: str,
      );
      // _logTime(stopwatch);
      // return;
    }
  }

  // Add all the layouts to the download queue excluding the [layoutID] if
  // that was specified. We don't want to download that layout twice.
  if (config.preload) {
    DebugLogger.instance.printInfo(
      'Config preload was specified during init, adding ${_publishModel!.updates.layouts.length - 1} layouts to the download queue...',
      name: name,
    );
    _downloadQueue.addAll(
      [..._publishModel!.updates.layouts.keys]..remove(layoutID),
    );
    DebugLogger.instance.printInfo(
      'All layouts during init download complete. ${_downloadQueue.length} layouts in queue.',
      name: name,
    );
  }

  // If a [layoutID] was specified for this initialization, then the Future
  // callback of this init function must complete once the layout has been
  // downloaded.
  // Otherwise we need to await for all layouts to be downloaded before
  // completing the Future.
  if (layoutID != null) {
    DebugLogger.instance.printInfo(
      'Layout [$layoutID] was specified during init. Processing download queue in the background...',
      name: name,
    );
    processDownloadQueue();
  } else {
    DebugLogger.instance.printInfo(
      'No layout was specified during init. Awaiting all layouts to be downloaded...',
      name: name,
    );
    await processDownloadQueue();
  }

  purgeDisabledLayouts(notify: true);

  _logTime(stopwatch);
}