UNPKG

@magnetarjs/plugin-firestore

Version:
129 lines (128 loc) 6.51 kB
import { getFirestoreCollectionPath, getFirestoreDocPath, } from '@magnetarjs/utils-firestore'; import { doc, onSnapshot } from 'firebase/firestore'; import { isPromise, isString } from 'is-what'; import { docSnapshotToDocMetadata, getQueryInstance } from '../helpers/getFirestore.js'; export function streamActionFactory(firestorePluginOptions) { return function ({ payload, collectionPath, docId, pluginModuleConfig, mustExecuteOnRead, writeLockMap, }) { const { added, modified, removed } = mustExecuteOnRead; const { db, debug } = firestorePluginOptions; // Extract onFirstData callback from payload if provided const onFirstData = payload?.onFirstData; let resolveStream; let rejectStream; const streaming = new Promise((resolve, reject) => { resolveStream = resolve; rejectStream = reject; }); let closeStream; let firstDataReceived = false; // in case of a doc module if (isString(docId)) { const documentPath = getFirestoreDocPath(collectionPath, docId, pluginModuleConfig, firestorePluginOptions); // prettier-ignore // Pool doc snapshots that arrive while waiting for write lock let pendingSnapshot = null; let isProcessing = false; const processDocSnapshot = (docSnapshot) => { // do nothing if the doc doesn't exist if (!docSnapshot.exists()) return; // serverChanges only const docData = docSnapshot.data(); const docMetadata = docSnapshotToDocMetadata(docSnapshot); if (docData) added(docData, docMetadata); }; closeStream = onSnapshot(doc(db, documentPath), async (docSnapshot) => { // even if `docSnapshot.metadata.hasPendingWrites` // we should always execute `added/modified` // because `core` handles overlapping calls for us // Store latest snapshot (only need the most recent for a single doc) pendingSnapshot = docSnapshot; // If already processing, let the current processor handle it if (isProcessing) return; // Check for write lock const collectionWriteLock = writeLockMap.get(collectionPath); if (collectionWriteLock && isPromise(collectionWriteLock.promise)) { isProcessing = true; await collectionWriteLock.promise; } // Process the latest pending snapshot if (pendingSnapshot) { const snapshot = pendingSnapshot; pendingSnapshot = null; processDocSnapshot(snapshot); } // Call onFirstData after processing (after write lock, whether doc exists or not) if (!firstDataReceived && onFirstData) { firstDataReceived = true; setTimeout(() => onFirstData({ empty: !docSnapshot.exists() }), 0); } isProcessing = false; }, rejectStream); } // in case of a collection module else if (!docId) { const _collectionPath = getFirestoreCollectionPath(collectionPath, pluginModuleConfig, firestorePluginOptions); // prettier-ignore const query = getQueryInstance(_collectionPath, pluginModuleConfig, db, debug); // Pool querySnapshots that arrive while waiting for write lock const pendingSnapshots = []; let isProcessing = false; closeStream = onSnapshot(query, async (querySnapshot) => { // even if `docSnapshot.metadata.hasPendingWrites` // we should always execute `added/modified` // because `core` handles overlapping calls for us // Add to pending pool pendingSnapshots.push(querySnapshot); // If already processing, let the current processor handle it if (isProcessing) return; // Check for write lock const collectionWriteLock = writeLockMap.get(collectionPath); if (collectionWriteLock && isPromise(collectionWriteLock.promise)) { isProcessing = true; await collectionWriteLock.promise; } // Merge all pending snapshots into a single map of docId -> final state const mergedDocs = new Map(); let snapshot = pendingSnapshots.shift(); while (snapshot) { snapshot .docChanges() .forEach((docChange) => { const docSnapshot = docChange.doc; const docData = docSnapshot.data(); const docMetadata = docSnapshotToDocMetadata(docSnapshot); // Always overwrite with the latest state for this doc mergedDocs.set(docSnapshot.id, { type: docChange.type, docData, docMetadata }); }); snapshot = pendingSnapshots.shift(); } // Process the merged final states for (const { type, docData, docMetadata } of mergedDocs.values()) { if (type === 'added' && docData) { added(docData, docMetadata); } if (type === 'modified' && docData) { modified(docData, docMetadata); } if (type === 'removed') { removed(docData, docMetadata); } } // Call onFirstData after processing (after write lock, whether collection has docs or not) if (!firstDataReceived && onFirstData) { firstDataReceived = true; setTimeout(() => onFirstData({ empty: querySnapshot.empty }), 0); } isProcessing = false; }, rejectStream); } function stop() { if (resolveStream) resolveStream(); closeStream(); } return { stop, streaming }; }; }