GregoryConrad / rearch-dart

Re-imagined approach to application design and architecture
https://pub.dev/packages/rearch
MIT License
88 stars 4 forks source link

Warm-up related Web weird issue #177

Closed busslina closed 4 months ago

busslina commented 4 months ago

Firstable I'm explaining my Warm-up logic so if you can see a better way to do it I will appreciate any advice. Secondly I will expose an only web (I believe) fatal issue that is related to the Warm-up.

My Warm-up master driven:

LoadingValue loadingModeCapsule(CapsuleHandle use) {
  final appState = use(appStateCapsule);
  final lang = use(languageCapsule);

  switch (appState.state) {
    case AppState.websocketConnecting:
      return (appState.extra as WebsocketConnectionState)
          .getLoadingMessage(use)
          .asLoadingValue;

    case AppState.checkingLogged:
      return LoadingValue.loading(lang.checkingAuthenticated.firstUpper);

    case AppState.notAuthenticated:
      break;

    case AppState.authenticated:
      break;

    case AppState.error:
      return LoadingValue.loading(lang.anErrorOccurred);
  }

  // Register info capsules
  {
    final res = use(getRegisterInfoLoadingValueCapsule)!;
    if (res.loading) return res;
  }

  // Data capsules
  {
    final res = use(getDataLoadingValueCapsule);
    if (res.loading) return res;
  }

  // Project
  return use(projectLoadingModeCapsule);
}

This way I proceed to warm-up different pieces at the moment/state I want it to load.

For data capsules, I created two side effects wrappers that help to cache, update and reset the wrapped data: asManagedData and asManagedAsyncData.

extension SideEffectRegistrarExtensionFwlib on SideEffectRegistrar {
  ManagedData<T> asManagedData<T>({
    required String name,
    required CapsuleReader capsuleReader,
    Option<T> initialValue = const None(),
    ManagedDataScope scope = ManagedDataScope.connected,
  }) {
    final cachedName = use.value(name);

    final data = use.data<Option<T>>(initialValue);

    final appState = capsuleReader(appStateCapsule).state;

    final log = capsuleReader(loggerCapsule);

    String msg(String msg) => 'asManagedData() -- $cachedName ($T) -- $msg';

    void reset() {
      data.value = initialValue;
    }

    void checkScope() {
      switch (scope) {
        case ManagedDataScope.connected:
          if (!appState.websocketConnected) {
            throw (msg(
              'Error: Trying to set new value while app state is not websocket connected. Is ${appState.asString}',
            ));
          }
        case ManagedDataScope.checkedLogged:
          if (!appState.checkedLogged) {
            throw (msg(
              'Error: Trying to set new value while app state is not checked logged. Is ${appState.asString}',
            ));
          }
        case ManagedDataScope.authenticated:
          if (!appState.authenticated) {
            throw (msg(
              'Error: Trying to set new value while app state is not authenticated. Is ${appState.asString}',
            ));
          }
      }
    }

    void setNewValue(T newValue) {
      checkScope();

      // Version check
      if (data.value.isSome && isSubtype<T, Versionable>()) {
        final versionableCurrentValue =
            (data.value as Some<T>).value as Versionable;
        final versionableNewValue = newValue as Versionable;

        if (!versionableNewValue.isNewerThan(versionableCurrentValue)) {
          log.warning(
            msg('Skipping due to version is not newer -- Current: ${versionableCurrentValue.getAssertedVersion()} -- Incoming: ${versionableNewValue.getAssertedVersion()}'),
          );
          return;
        }
      }

      data.value = Some(newValue);
    }

    // Resetting
    {
      // On not connected
      if (scope.connected && !appState.websocketConnected) {
        reset();
      }

      // On not checked logged
      else if (scope.checkedLogged && !appState.checkedLogged) {
        reset();
      }

      // On not authenticated
      else if (scope.authenticated && !appState.authenticated) {
        reset();
      }
    }

    return ManagedData(
      getNullableValue: () => data.value.asNullable(),
      getValue: () => data.value.asNullable()!,
      setValue: setNewValue,
    );
  }

  ManagedAsyncData<T> asManagedAsyncData<T>({
    required String name,
    required Future<T>? future,
    required CapsuleReader capsuleReader,
    ManagedDataScope scope = ManagedDataScope.connected,
  }) {
    final cachedName = use.value(name);

    final asyncValue = use.nullableFuture(future);
    final overrideData = use.data<Option<T>>(const None());

    final appState = capsuleReader(appStateCapsule).state;

    final log = capsuleReader(loggerCapsule);

    String msg(String msg) =>
        'asManagedAsyncData() -- $cachedName ($T) -- $msg';

    void reset() {
      overrideData.value = const None();
    }

    void checkScope() {
      switch (scope) {
        case ManagedDataScope.connected:
          if (!appState.websocketConnected) {
            throw (msg(
              'Error: Trying to set new value while app state is not websocket connected. Is ${appState.asString}',
            ));
          }
        case ManagedDataScope.checkedLogged:
          if (!appState.checkedLogged) {
            throw (msg(
              'Error: Trying to set new value while app state is not checked logged. Is ${appState.asString}',
            ));
          }
        case ManagedDataScope.authenticated:
          if (!appState.authenticated) {
            throw (msg(
              'Error: Trying to set new value while app state is not authenticated. Is ${appState.asString}',
            ));
          }
      }
    }

    T? getNullableValue() {
      final overrideValue = overrideData.value;
      if (overrideValue is Some<T>) {
        return overrideValue.value;
      }

      return asyncValue?.asAsyncData?.data;
    }

    T getValue() => getNullableValue() as T;

    void setNewValue(T newValue) {
      checkScope();

      final currentData = getNullableValue();

      // Version check
      if (currentData != null && isSubtype<T, Versionable>()) {
        final versionableCurrentValue = currentData as Versionable;
        final versionableNewValue = newValue as Versionable;

        if (!versionableNewValue.isNewerThan(versionableCurrentValue)) {
          log.warning(
            msg('Skipping due to version is not newer -- Current: ${versionableCurrentValue.getAssertedVersion()} -- Incoming: ${versionableNewValue.getAssertedVersion()}'),
          );
          return;
        }
      }

      overrideData.value = Some(newValue);
    }

    // Resetting
    {
      // On not connected
      if (scope.connected && !appState.websocketConnected) {
        reset();
      }

      // On not checked logged
      else if (scope.checkedLogged && !appState.checkedLogged) {
        reset();
      }

      // On not authenticated
      else if (scope.authenticated && !appState.authenticated) {
        reset();
      }
    }

    return ManagedAsyncData(
      asyncValue: asyncValue,
      getValue: getValue,
      setValue: setNewValue,
    );
  }
}

class ManagedData<T> {
  ManagedData({
    required this.getNullableValue,
    required this.getValue,
    required this.setValue,
  });

  T? Function() getNullableValue;
  T Function() getValue;
  void Function(T) setValue;
}

enum ManagedDataScope {
  connected,
  checkedLogged,
  authenticated,
}

extension ManagedDataScopeExtension on ManagedDataScope {
  bool get connected => this == ManagedDataScope.connected;
  bool get checkedLogged => this == ManagedDataScope.checkedLogged;
  bool get authenticated => this == ManagedDataScope.authenticated;
}

class ManagedAsyncData<T> {
  ManagedAsyncData({
    required this.asyncValue,
    required this.getValue,
    required this.setValue,
  });

  AsyncValue<T>? asyncValue;
  T Function() getValue;
  void Function(T) setValue;
}

This logic works like a charm.... Except for a very weird web issue:

Web Weird Issue

In the past I have seen that a web browser (in my case Edge) is like in a sleep mode when minimized. It was not big deal because when I put back in front you just see a few milliseconds the old screen (maybe it was connected to websocket 10 seconds before, but until you put in front it gets stuck with the loading widget, but not big deal).

But what is happening now? I'm testing WS connection lost and reconnection. If I do this with the web page in the front of the screen everything is good. But If the web page is minimezed I got this error:

Error: asManagedData() -- Online web devices count
(Versioned<int>) -- Error: Trying to set new value while app
state is not websocket connected. Is Websocket connecting

You can see where it is thrown in the code I shared.

So it is crazy because it means that when the web page is in the background some of the code is running and some other code isn't (maybe the one related with the visuals...).

Any thoughts on this?

Info

Web renderer: html (I have to check with canvaskit) Updated: Checked with canvaskit: Same error

Updated I: Video demo

https://streamable.com/ftmvtw

Updated II

Tested with Firefox in production: Same error Tested with Chrome in production: Same error (or worst)

Sources

When I minimize the browser window, the script stops. How fix it? How to prevent JavaScript from running slow on minimized browser window?

busslina commented 4 months ago

https://stackoverflow.com/a/76068658/4681977

Tabs running code that uses real-time network connections (WebSockets and WebRTC) are not slowed down to prevent these connections from being closed unexpectedly.

busslina commented 4 months ago

https://www.reddit.com/r/flutterhelp/comments/1cr2d68/very_very_weirdannoying_issue_on_flutter_web/

busslina commented 4 months ago

My bad 97%