snapshots method

Stream<QuerySnapshot<T>> snapshots(
  1. {bool includeMetadataChanges = false,
  2. ListenSource source = ListenSource.defaultSource}
)

Returns a stream of snapshots for the query.

Parameters:

  • includeMetadataChanges: Whether to include metadata changes.
  • source: The source for listening.

Returns: A stream of QuerySnapshot events.

Example:

query.snapshots().listen((snapshot) { /* handle snapshot */ });

Implementation

Stream<QuerySnapshot<T>> snapshots({
  bool includeMetadataChanges = false,
  ListenSource source = ListenSource.defaultSource,
}) {
  this._rt = 1;
  late StreamController<QuerySnapshot<T>> controller;
  Timer? _pollingTimer;

  var auth = FusabaseAuth.instanceFor(app: this._oracledb.app);
  var email = auth.currentUser?.email ?? "";
  var userLvlPayload = {
    "path": this._path,
    "conditions": this._conditions,
    "explicitOrder": this._explicitOrder,
    "access_token": email,
  };

  // this query id is used to store in the cache (user level)
  int queryIdInt = createHashFromJson(userLvlPayload);
  String queryId = queryIdInt.toString();

  var socketPayload = {
    "path": this._path,
    "conditions": this._conditions,
    "explicitOrder": this._explicitOrder,
  };

  // this query id is used to register listener (common for each query)
  int queryIdSocketInt = createHashFromJson(socketPayload);
  String queryIdSocket = queryIdSocketInt.toString();

  // this query id is used to store the notifiers (unique each time)
  String localQueryId = generateRandomHash();

  Future<QuerySnapshot<T>> firstPoll() async {
    dynamic oldData = await this._oracledb._snapStore.getData(
      queryId,
      oracledb,
    );

    QuerySnapshot newSnap = await this.get();

    if (oldData == null) {
      //first snapshot
      await this._oracledb._snapStore.insertData({
        "queryId": queryId,
        "snap": newSnap,
        "type": "collection",
        "path": this.path,
      });
    } else {
      await this._oracledb._snapStore.updateData({
        "queryId": queryId,
        "snap": newSnap,
        "type": "collection",
        "path": this.path,
      });
    }
    return newSnap as QuerySnapshot<T>;
  }

  bool uselongPolling =
      _oracledb.settings.webExperimentalForceLongPolling ?? true;
  if (_oracledb.settings.webExperimentalForceLongPolling ?? true) {
    uselongPolling = true;
  }

  //uselongPolling = false;

  if (uselongPolling) {
    controller = StreamController<QuerySnapshot<T>>.broadcast(
      onListen: () async {
        controller.add(await firstPoll());
      },
      onCancel: () {
        _pollingTimer?.cancel();
      },
    );

    final pollingInterval = _safeLongPollingInterval(this._oracledb.settings);

    Future<void> longPolling() async {
      dynamic oldData = await this._oracledb._snapStore.getData(
        queryId,
        oracledb,
      );
      QuerySnapshot newSnap = await this.get();
      if (oldData == null) {
        //first snapshot
        await this._oracledb._snapStore.insertData({
          "queryId": queryId,
          "snap": newSnap,
          "type": "collection",
          "path": this.path,
        });
        controller.add(newSnap as QuerySnapshot<T>);
        return;
      }

      //create version maps for old and new snapshot docs
      var oldVersionMap = {};
      var newVersionMap = {};

      //index map for old documents
      var oldIndexMap = {};

      for (var i = 0; i < newSnap._docs.length; i++) {
        newVersionMap[newSnap._docs[i].id] =
            newSnap._docs[i]._otherMetadata["ASOF"] != null
                ? BigInt.parse(newSnap._docs[i]._otherMetadata["ASOF"])
                : newSnap._docs[i]._otherMetadata["VERSION"];
      }
      for (var i = 0; i < oldData["snap"]._docs.length; i++) {
        oldVersionMap[oldData["snap"]._docs[i].id] =
            oldData["snap"]._docs[i]._otherMetadata["ASOF"] != null
                ? BigInt.parse(
                  oldData["snap"]._docs[i]._otherMetadata["ASOF"],
                )
                : oldData["snap"]._docs[i]._otherMetadata["VERSION"];
        oldIndexMap[oldData["snap"]._docs[i].id] = i;
      }

      //create doc changes
      List<DocumentChange> docsChanged = [];
      for (var i = 0; i < newSnap._docs.length; i++) {
        if (oldVersionMap[newSnap._docs[i].id] != null &&
            newVersionMap[newSnap._docs[i].id] >
                oldVersionMap[newSnap._docs[i].id]) {
          docsChanged.add(
            DocumentChange(
              doc: DocumentSnapshot(
                {
                  "DOCUMENT": newSnap._docs[i]._data,
                  "OID": newSnap._docs[i].id,
                  "VERSION": newSnap._docs[i].version,
                },
                newSnap._docs[i]._reference,
                newSnap._docs[i]._metadata,
                newSnap._docs[i].rowID,
              ),
              type: DocumentChangeType.modified,
              oldIndex: oldIndexMap[newSnap._docs[i].id],
              newIndex: i,
            ),
          );
        } else if (oldVersionMap[newSnap._docs[i].id] == null) {
          docsChanged.add(
            DocumentChange(
              doc: DocumentSnapshot(
                {
                  "DOCUMENT": newSnap._docs[i]._data,
                  "OID": newSnap._docs[i].id,
                  "VERSION": newSnap._docs[i].version,
                },
                newSnap._docs[i]._reference,
                newSnap._docs[i]._metadata,
                newSnap._docs[i].rowID,
              ),
              type: DocumentChangeType.added,
              oldIndex: -1,
              newIndex: i,
            ),
          );
        }
      }
      for (var i = 0; i < oldData["snap"]._docs.length; i++) {
        if (newVersionMap[oldData["snap"]._docs[i].id] == null) {
          docsChanged.add(
            DocumentChange(
              doc: DocumentSnapshot(
                {
                  "DOCUMENT": oldData["snap"]._docs[i]._data,
                  "OID": oldData["snap"]._docs[i].id,
                  "VERSION": oldData["snap"]._docs[i].version,
                },
                oldData["snap"]._docs[i].reference,
                oldData["snap"]._docs[i].metadata,
                oldData["snap"]._docs[i].rowID,
              ),
              type: DocumentChangeType.removed,
              oldIndex: i,
              newIndex: -1,
            ),
          );
        }
      }

      newSnap._docChanges = docsChanged;

      await this._oracledb._snapStore.updateData({
        "queryId": queryId,
        "snap": newSnap,
        "type": "collection",
        "path": this.path,
      });

      if (docsChanged.isNotEmpty) {
        controller.add(newSnap as QuerySnapshot<T>);
      }
      return;
    }

    _pollingTimer = Timer(pollingInterval, () {
      longPolling();

      _pollingTimer = Timer.periodic(pollingInterval, (Timer timer) {
        longPolling();
      });
    });

    return controller.stream;
  } else {
    bool isCancelled = false;
    bool hasRegisteredRealtimeListener = false;
    dynamic payload = {
      "queryId": queryIdSocketInt,
      "status": 1,
      "payload": socketPayload,
    };

    void addToController(dynamic snap) {
      if (!controller.isClosed) {
        controller.add(snap as QuerySnapshot<T>);
      }
    }

    controller = StreamController<QuerySnapshot<T>>.broadcast(
      onListen: () async {
        isCancelled = false;
        await _oracledb.preSnapshot();
        if (isCancelled) {
          return;
        }
        this._oracledb._queryIdMap[queryIdSocket] = queryId;
        List<String> localQueryIds = this._oracledb._localQueryIdMap
            .putIfAbsent(queryId, () => []);
        if (!localQueryIds.contains(localQueryId)) {
          localQueryIds.add(localQueryId);
        }
        this._oracledb._snapshotControllerMap[localQueryId] = addToController;
        hasRegisteredRealtimeListener = true;
        this._oracledb.sendMessageSocket(payload);
        dynamic fsnap = await firstPoll();
        if (!isCancelled && !controller.isClosed) {
          controller.add(fsnap);
        }
      },
      onCancel: () {
        isCancelled = true;
        if (!hasRegisteredRealtimeListener) {
          return;
        }
        hasRegisteredRealtimeListener = false;
        if (this._oracledb._snapshotControllerMap.containsKey(localQueryId)) {
          this._oracledb._snapshotControllerMap.remove(localQueryId);
        }
        if (this._oracledb._localQueryIdMap.containsKey(queryId) &&
            this._oracledb._localQueryIdMap[queryId]!.contains(
              localQueryId,
            )) {
          this._oracledb._localQueryIdMap[queryId]?.remove(localQueryId);
        }
        if (!this._oracledb._localQueryIdMap.containsKey(queryId) ||
            this._oracledb._localQueryIdMap[queryId]!.isEmpty) {
          this._oracledb._localQueryIdMap.remove(queryId);
          this._oracledb._queryIdMap.remove(queryIdSocket);
          dynamic unsubPayload = {
            "queryId": queryIdSocketInt,
            "status": 0,
            "payload": socketPayload,
          };
          this._oracledb.sendMessageSocket(unsubPayload);
        }
      },
    );

    return controller.stream;
  }
}