UNPKG

@nozbe/watermelondb

Version:

Build powerful React Native and React web apps that scale from hundreds to tens of thousands of records and remain fast

159 lines (144 loc) 6.35 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); exports.__esModule = true; exports.default = subscribeToQueryWithColumns; var _identicalArrays = _interopRequireDefault(require("../../utils/fp/identicalArrays")); var _subscribeToSimpleQuery = _interopRequireDefault(require("../subscribeToSimpleQuery")); var _subscribeToQueryReloading = _interopRequireDefault(require("../subscribeToQueryReloading")); var _canEncode = _interopRequireDefault(require("../encodeMatcher/canEncode")); var getRecordState = function (record, columnNames) { var state = []; var raw = record._raw; for (var i = 0, len = columnNames.length; i < len; i++) { // $FlowFixMe state.push(raw[columnNames[i]]); } return state; }; // Invariant: same length and order of keys! var recordStatesEqual = _identicalArrays.default; // Observes the given observable list of records, and in those records, // changes to given `rawFields` // // Emits a list of records when: // - source observable emits a new list // - any of the records in the list has any of the given fields changed // // TODO: Possible future optimizations: // - simpleObserver could emit added/removed events, and this could operate on those instead of // re-deriving the same thing. For reloadingObserver, a Rx adapter could be fitted // - multiple levels of array copying could probably be omitted function subscribeToQueryWithColumns(query, columnNames, subscriber) { // State kept for comparison between emissions var unsubscribed = false; var sourceIsFetching = true; // do not emit record-level changes while source is fetching new data var hasPendingColumnChanges = false; var firstEmission = true; var observedRecords = []; var recordStates = new Map(); var emitCopy = function (records) { unsubscribed || subscriber(records.slice(0)); }; // NOTE: // Observing both the source subscription and changes to columns is very tricky // if we want to avoid unnecessary emissions (we do, because that triggers wasted app renders). // The compounding factor is that we have two methods of observation: simpleObserver which is // synchronous, and reloadingObserver, which is asynchronous. // // For reloadingObserver, we use `reloadingObserverWithStatus` to be notified that an async DB query // has begun. If it did, we will not emit column-only changes until query has come back. // // For simpleObserver, we need to configure it to always emit on collection changes. This is a // workaround to solve a race condition - collection observation for column check will always // emit first, but we don't know if the list of observed records isn't about to change, so we // flag, and wait for source response. // // FIXME: The above explanation is outdated in practice because modern WatermelonDB uses synchronous // adapters... However, JSI on Android isn't yet fully shipped (so this is currently broken), and // we may get back to some asynchronicity where appropriate... // prepare source observable // TODO: On one hand it would be nice to bring in the source logic to this function to optimize // on the other, it would be good to have source provided as Observable, not Query // so that we can reuse cached responses -- but they don't have compatible format var canUseSimpleObservation = (0, _canEncode.default)(query.description); var subscribeToSource = canUseSimpleObservation ? function (observer) { return (0, _subscribeToSimpleQuery.default)(query, observer, true); } : function (observer) { return (0, _subscribeToQueryReloading.default)(query, observer, true); }; // Observe changes to records we have on the list var collectionUnsubscribe = query.collection.experimentalSubscribe( // eslint-disable-next-line prefer-arrow-callback function (changeSet) { var hasColumnChanges = false; // Can't use `Array.some`, because then we'd skip saving record state for relevant records changeSet.forEach(function ({ record: record, type: type }) { // See if change is relevant to our query if ('updated' !== type) { return; } var previousState = recordStates.get(record.id); if (!previousState) { return; } // Check if record changed one of its observed fields var newState = getRecordState(record, columnNames); if (!recordStatesEqual(previousState, newState)) { recordStates.set(record.id, newState); hasColumnChanges = true; } }); if (hasColumnChanges) { if (sourceIsFetching || !!canUseSimpleObservation) { // Mark change; will emit on source emission to avoid duplicate emissions hasPendingColumnChanges = true; } else { emitCopy(observedRecords); } } }, { name: 'subscribeToQueryWithColumns', query: query, columnNames: columnNames }); // Observe the source records list (list of records matching a query) // eslint-disable-next-line prefer-arrow-callback var sourceUnsubscribe = subscribeToSource(function (recordsOrStatus) { // $FlowFixMe if (false === recordsOrStatus) { sourceIsFetching = true; return; } sourceIsFetching = false; // Emit changes if one of observed columns changed OR list of matching records changed var records = recordsOrStatus; var shouldEmit = firstEmission || hasPendingColumnChanges || !(0, _identicalArrays.default)(records, observedRecords); hasPendingColumnChanges = false; firstEmission = false; // Find changes, and save current list for comparison on next emission var arrayDifference = require('../../utils/fp/arrayDifference').default; var { added: added, removed: removed } = arrayDifference(observedRecords, records); observedRecords = records; // Unsubscribe from records removed from list removed.forEach(function (record) { recordStates.delete(record.id); }); // Save current record state for later comparison added.forEach(function (newRecord) { recordStates.set(newRecord.id, getRecordState(newRecord, columnNames)); }); // Emit shouldEmit && emitCopy(records); }); return function () { unsubscribed = true; sourceUnsubscribe(); collectionUnsubscribe(); }; }