Je suis coincé avec cette erreur depuis 3 jours et j'ai littéralement tout essayé, essayé de structurer les promesses de 1000 façons mais rien ne semble fonctionner. Peut-être que je perds la «vue d'ensemble», alors j'espère que de nouveaux yeux aideront. Merci d'avoir lu:
J'ai une fonction planifiée en cours d'exécution dans Firebase Cloud Functions. Ce que le code essaie d'accomplir est
Dans ma dernière tentative (copiée ci-dessous), je vérifie s'il y a un document dans l'instantané (ce qui signifierait qu'il y a un autre document du même type, donc le document n'a pas à être supprimé). Ensuite, si res! == true, je supprimerais le document.
Le problème est que pour une raison quelconque, res n'est jamais vrai ... peut-être que la promesse "res" se résout avant la promesse "instantané"?
const functions = require('firebase-functions'); const admin = require('firebase-admin'); admin.initializeApp(); exports.scheduledFunction = functions.pubsub .schedule('0 23 * * *').timeZone('Europe/Madrid') .onRun( async (context) => { const expiredDocs = await admin.firestore().collection('PROMOTIONS_INFO') .where('active','==',true) .where('expiration', '<=', new Date()) .get() .then(async (snapshot) => { await Promise.all(snapshot.docs.map( async (doc) => { doc.ref.update({active: false}) const type = doc.data().business.type const id = doc.data().id const exists = await admin.firestore().collection('PROMOTIONS_INFO') .where('active','==',true) .where('business.type','==', type) .where('id', '!=', id) .limit(1) .get() .then((snapshot) => { snapshot.docs.map((doc)=>{return true}) }).then(async (res) => { res===true ? null : (await admin.firestore().collection('PROMOTIONS_INFO').doc('types') .update('types', admin.firestore.FieldValue.arrayRemove(type))) }) })) }); });
3 Réponses :
Je suppose dans ce cas que res
est indéfini et évalue comme faux.
Avant votre .then
promise avec le paramètre res
, vous avez une promesse .then
précédente qui retourne void:
//... .then((snapshot) => { return snapshot.docs.map((doc)=>{return true}) }).then(async (res) => { res===true ? null : (await admin.firestore().collection('PROMOTIONS_INFO').doc('types') .update('types', admin.firestore.FieldValue.arrayRemove(type))) }) //...
En fonction de votre intention, vous devrez renvoyer une valeur dans cette promesse précédente. Il semble que vous créez un tableau de valeurs booléennes correspondant à la longueur du nombre de snapshot.docs
que vous avez, donc si vous mettez une instruction return simplement dans la clause .then
précédente, res
serait quelque chose comme, [true, true, true, true, /* ... */]
//... .then((snapshot) => { snapshot.docs.map((doc)=>{return true}) // <--- This is not actually returning a resolved value }).then(async (res) => { res===true ? null : (await admin.firestore().collection('PROMOTIONS_INFO').doc('types') .update('types', admin.firestore.FieldValue.arrayRemove(type))) }) //...
snapshot.docs.map((doc)=>{return true})
renvoie Array comme [true, false]
pas booléen comme true
.
Donc .then( async (res) => { res===true ? null : await admin.firestore(...
ne peut pas fonctionner. Et
Vous devriez peut-être modifier comme suit.
.then((snapshot) => snapshot.docs.length > 0 ? null : await admin.firestore(...
Pour obtenir le résultat souhaité, vous pouvez envisager d'utiliser les écritures par lots et de diviser votre code en étapes distinctes.
Un ensemble d'étapes possibles est:
Dans les étapes ci-dessus, l'étape 3 peut utiliser les écritures par lots et l'étape 6 peut utiliser la transformation de champ arrayRemove()
qui peut supprimer plusieurs éléments à la fois pour alléger le fardeau de votre base de données.
class MultiBatch { constructor(dbRef) { this.dbRef = dbRef; this.batchOperations = []; this.batches = [this.dbRef.batch()]; this.currentBatch = this.batches[0]; this.currentBatchOpCount = 0; this.committed = false; } /** Used when for basic update operations */ update(ref, changesObj) { if (this.committed) throw new Error('MultiBatch already committed.'); if (this.currentBatchOpCount + 1 > 500) { // operation limit exceeded, start a new batch this.currentBatch = this.dbRef.batch(); this.currentBatchOpCount = 0; this.batches.push(this.currentBatch); } this.currentBatch.update(ref, changesObj); this.currentBatchOpCount++; } /** Used when an update contains serverTimestamp, arrayUnion, arrayRemove, increment or decrement (which all need to be counted as 2 operations) */ transformUpdate(ref, changesObj) { if (this.committed) throw new Error('MultiBatch already committed.'); if (this.currentBatchOpCount + 2 > 500) { // operation limit exceeded, start a new batch this.currentBatch = this.dbRef.batch(); this.currentBatchOpCount = 0; this.batches.push(this.currentBatch); } this.currentBatch.update(ref, changesObj); this.currentBatchOpCount += 2; } commit() { this.committed = true; return Promise.all(this.batches.map(batch => batch.commit())); } }
Remarque: la vérification des erreurs est omise et doit être mise en œuvre.
Si vous prévoyez d'atteindre la limite de 500 opérations par lot, vous pouvez ajouter un wrapper autour des lots afin qu'ils soient automatiquement divisés selon les besoins. Un emballage possible est inclus ici:
const functions = require('firebase-functions'); const admin = require('firebase-admin'); admin.initializeApp(); exports.scheduledFunction = functions.pubsub .schedule('0 23 * * *').timeZone('Europe/Madrid') .onRun( async (context) => { // get instance of Firestore to use below const db = admin.firestore(); // this is reused often, so initialize it once. const promotionsInfoColRef = db.collection('PROMOTIONS_INFO'); // find all documents that are active and have expired. const expiredDocsQuerySnapshot = await promotionsInfoColRef .where('active','==',true) .where('expiration', '<=', new Date()) .get(); if (expiredDocsQuerySnapshot.empty) { // no expired documents, log the result console.log(`No documents have expired recently.`); return; // done } // initialize an object to store all the types to be checked // this helps ensure each type is checked only once const typesToCheckObj = {}; // initialize a batched write to make changes all at once, rather than call out to Firestore multiple times // note: batches are limited to 500 read/write operations in a single batch const makeDocsInactiveBatch = db.batch(); // for each snapshot, add their type to typesToCheckObj and update them to inactive expiredDocsQuerySnapshot.forEach(doc => { const type = doc.get("business.type"); // rather than use data(), parse only the property you need. typesToCheckObj[type] = true; // add this type to the ones to check makeDocsInactiveBatch.update(doc.ref, { active: false }); // add the "update to inactive" operation to the batch }); // update database for all the now inactive documents all at once. // we update these documents first, so that the type check are done against actual "active" documents. await makeDocsInactiveBatch.commit(); // this is a unique array of the types encountered above // this can now be used to check each type ONCE, instead of multiple times const typesToCheckArray = Object.keys(typesToCheckObj); // check each type and return types that have no active promotions const typesToRemoveArray = (await Promise.all( typesToCheckArray.map((type) => { return promotionsInfoColRef .where('active','==',true) .where('business.type','==', type) .limit(1) .get() .then((querySnapshot) => querySnapshot.empty ? type : null) // if empty, include the type for removal }) )) .filter((type) => type !== null); // filter out the null values that represent types that don't need removal // typesToRemoveArray is now a unique list of strings, containing each type that needs to be removed if (typesToRemoveArray.length == 0) { // no types need removing, log the result console.log(`Updated ${expiredDocsQuerySnapshot.size} expired documents to "inactive" and none of the ${typesToCheckArray.length} unique types encountered needed to be removed.`); return; // done } // get the types document reference const typesDocRef = promotionsInfoColRef.doc('types'); // use the arrayRemove field transform to remove all the given types at once await typesDocRef.update({types: admin.firestore.FieldValue.arrayRemove(...typesToRemoveArray) }); // log the result console.log(`Updated ${expiredDocsQuerySnapshot.size} expired documents to "inactive" and ${typesToRemoveArray.length}/${typesToCheckArray.length} unique types encountered needed to be removed.\n\nThe types removed: ${typesToRemoveArray.sort().join(", ")}`);
Pour l'utiliser, remplacez db.batch()
dans le code d'origine par le new MultiBatch(db)
. Si une mise à jour dans le lot (comme someBatch.update(ref, { ... })
) contient une transformation de champ (telle que FieldValue.arrayRemove()
), assurez-vous d'utiliser someMultiBatch.transformUpdate(ref, { ... })
place pour qu'une seule mise à jour soit correctement comptée comme 2 opérations (une lecture et une écriture).
Ça marche! Je ne saurais trop vous remercier pour votre temps et votre réponse complète. Je pense que, étant donné que nous avons les typesToRemoveArray, peut-être un moyen d'éviter autant de lectures / écritures sur la base de données serait de lire le document Types, de fusionner les deux tableaux dans le code, puis de l'écrire dans la base de données. Cela aurait-il des conséquences sur l'approche multibatch? De plus, avez-vous des considérations concernant la durée de fonctionnement?
@Mireia Vous avez tout à fait raison. En revisitant le code avec un œil neuf aujourd'hui, je me suis souvenu que arrayRemove
prend en charge plusieurs éléments à la fois - lorsqu'ils sont passés en tant qu'arguments séparés - ce qui peut être réalisé à l'aide de l'opérateur spread ( ...
). Cela élimine complètement la deuxième opération par lots. En termes de temps d'exécution, le code tel qu'il est ci-dessus est susceptible d'être le plus léger que vous puissiez atteindre. La partie la plus lente consistera à vérifier si les types existent toujours lorsque vous en avez un grand nombre à vérifier - mais vérifier chaque type une fois permet de maintenir ces performances à un niveau bas.
Si vous commencez à avoir des délais d'attente parce que les mises à jour prennent plus de 60 secondes, vous pouvez soit augmenter le délai d' expiration de la fonction jusqu'à 9 minutes, soit l'exécuter plusieurs fois par jour (ce dernier est le meilleur choix). Changez simplement 0 23 * * *
en 0 5,11,17,23 * * *
pour l'exécuter toutes les 6 heures.
C'est génial! Merci. Par curiosité, savez-vous si l'utilisation de arrayRemove avec plusieurs éléments compte pour 1 lecture et écriture ou 1x élément?
Vous devez
return
éléments de toutes vos fonctions si vous comptez utiliser les fonctions fléchées de style() => { /* code */ }
. Il y a aussi une "pyramide de malheur" qui se développe ici que les promesses et l'async / attendent étaient censés éliminer.expiredDocs
si vous pouvez attendre la collectionexpiredDocs
après la fin.get()
. Supprimez le.then()
. Alors attendez la collectionexists
. Ensuite, bouclez dessus et exécutez la mise à jour sur chaque valeur.