watchOneNotifier method

  1. @protected
DataStateNotifier<T?> watchOneNotifier(
  1. String key, {
  2. bool? remote,
  3. Map<String, dynamic>? params,
  4. Map<String, String>? headers,
  5. AlsoWatch<T>? alsoWatch,
  6. String? finder,
  7. DataRequestLabel? label,
})
inherited

Implementation

@protected
DataStateNotifier<T?> watchOneNotifier(
  String key, {
  bool? remote,
  Map<String, dynamic>? params,
  Map<String, String>? headers,
  AlsoWatch<T>? alsoWatch,
  String? finder,
  DataRequestLabel? label,
}) {
  final id = graph.getIdForKey(key);

  remote ??= _remote;
  final maybeFinder = _internalHolder?.finders[finder]?.call(this);
  final finderFn = maybeFinder is DataFinderOne<T> ? maybeFinder : findOne;

  // we can't use `findOne`'s default internal label
  // because we need a label to handle events
  label ??=
      DataRequestLabel('watchOne', id: key.detypify(), type: internalType);

  var alsoWatchPairs = <List<String>>{};

  // closure to get latest model and watchable relationship pairs
  T? _getUpdatedModel() {
    final model = localAdapter.findOne(key);
    if (model != null) {
      // get all metas provided via `alsoWatch`
      final metas = alsoWatch
          ?.call(RelationshipGraphNode<T>())
          .whereType<RelationshipMeta>();

      // recursively get applicable watch key pairs for each meta -
      // from top to bottom (e.g. `p`, `p.familia`, `p.familia.cottage`)
      alsoWatchPairs = {
        ...?metas
            ?.map((meta) => _getPairsForMeta(meta._top, model))
            .filterNulls
            .expand((_) => _)
      };
    } else {
      // if there is no model nothing should be watched, reset pairs
      alsoWatchPairs = {};
    }
    return model;
  }

  final notifier = DataStateNotifier<T?>(
    data: DataState(_getUpdatedModel(), isLoading: remote!),
  );

  final alsoWatchNames = alsoWatch
          ?.call(RelationshipGraphNode<T>())
          .whereType<RelationshipMeta>()
          .map((m) => m.name) ??
      {};
  log(label,
      'initializing${alsoWatchNames.isNotEmpty ? ' (and also watching: ${alsoWatchNames.join(', ')})' : ''}');

  notifier._reloadFn = () async {
    if (!notifier.mounted || id == null) return;

    if (remote!) {
      notifier.updateWith(isLoading: true);
    }

    final model = await finderFn(
      id,
      remote: remote,
      params: params,
      headers: headers,
      label: label,
      onError: (e, label, _) async {
        try {
          await onError<T>(e, label);
        } on DataException catch (err) {
          e = err;
        } catch (_) {
          rethrow;
        }
        if (notifier.mounted) {
          notifier.updateWith(isLoading: false, exception: e);
        }
        return null;
      },
    );
    // trigger doneLoading to ensure state is updated with isLoading=false
    final modelKey = model?._key;
    if (remote && modelKey != null) {
      graph._notify([modelKey, label.toString()],
          type: DataGraphEventType.doneLoading);
    }
  };

  // trigger local + async loading
  notifier.reload();

  // local buffer useful to reduce amount of notifier updates
  var bufferModel = notifier.data.model;

  final throttleDuration = ref.read(graphNotifierThrottleDurationProvider);
  final throttledGraph = graph.throttle(() => throttleDuration);

  final states = <DataState<T?>>[];

  // start listening to graph for further changes
  final dispose = throttledGraph.addListener((events) {
    if (!notifier.mounted) return;

    // get the latest updated model with watchable relationships
    // (_alsoWatchPairs) in order to determine whether there is
    // something that will cause an event (with the introduction
    // of `andEach` even seemingly unrelated models could trigger)
    bufferModel = _getUpdatedModel();

    key = bufferModel?._key ?? key;

    for (final event in events) {
      if (event.keys.contains(key)) {
        // handle done loading
        if (notifier.data.isLoading &&
            event.keys.last == label.toString() &&
            event.type == DataGraphEventType.doneLoading) {
          states
              .add(DataState(bufferModel, isLoading: false, exception: null));
        }

        // add/update
        if (event.type == DataGraphEventType.addNode ||
            event.type == DataGraphEventType.updateNode) {
          if (notifier.data.isLoading == false) {
            log(label!, 'added/updated node ${event.keys}', logLevel: 2);
            states.add(DataState(
              bufferModel,
              isLoading: notifier.data.isLoading,
              exception: notifier.data.exception,
              stackTrace: notifier.data.stackTrace,
            ));
          }
        }

        // temporarily restore removed pair so that watchedRelationshipUpdate
        // has a chance to apply the update
        if (event.type == DataGraphEventType.removeEdge &&
            !event.keys.first.startsWith('id:')) {
          alsoWatchPairs.add(event.keys);
        }
      }

      // handle deletion
      if ([DataGraphEventType.removeNode, DataGraphEventType.clear]
              .contains(event.type) &&
          bufferModel == null) {
        log(label!, 'removed node ${event.keys}', logLevel: 2);
        states.add(DataState(
          null,
          isLoading: notifier.data.isLoading,
          exception: notifier.data.exception,
          stackTrace: notifier.data.stackTrace,
        ));
      }

      // updates on watched relationships condition
      final watchedRelationshipUpdate = event.type.isEdge &&
          alsoWatchPairs
              .where((pair) =>
                  pair.sorted().toString() == event.keys.sorted().toString())
              .isNotEmpty;

      // updates on watched models (of relationships) condition
      final watchedModelUpdate = event.type.isNode &&
          alsoWatchPairs
              .where((pair) => pair.contains(event.keys.first))
              .isNotEmpty;

      // if model is loaded and any condition passes, notify update
      if (notifier.data.isLoading == false &&
          (watchedRelationshipUpdate || watchedModelUpdate)) {
        log(label!, 'relationship update ${event.keys}', logLevel: 2);
        states.add(DataState(
          bufferModel,
          isLoading: notifier.data.isLoading,
          exception: notifier.data.exception,
          stackTrace: notifier.data.stackTrace,
        ));
      }
    }

    _updateFromStates(states, notifier);
  });

  notifier.onDispose = () {
    log(label!, 'disposing');
    dispose();
  };
  return notifier;
}