firebase distributed counter extension in cloud functions
Asked Answered
T

2

3

I am trying to implement Firebase's distributed counter extension in a cloud function. The idea is to detect when a document is created, then a counter inside a document is added. Below is my cloud function:

exports.bookmark_increment = functions.firestore.document('questions/{question_id}/bookmarks/{user_uid}')
  .onCreate(async (snap, context) => {

    // compiled client sample code for increment counter
  var sharded = function(t) { var e = {}; function r(n) { if (e[n]) return e[n].exports; var o = e[n] = { i: n, l: !1, exports: {} }; return t[n].call(o.exports, o, o.exports, r), o.l = !0, o.exports } return r.m = t, r.c = e, r.d = function(t, e, n) { r.o(t, e) || Object.defineProperty(t, e, { enumerable: !0, get: n }) }, r.r = function(t) { "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(t, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(t, "__esModule", { value: !0 }) }, r.t = function(t, e) { if (1 & e && (t = r(t)), 8 & e) return t; if (4 & e && "object" == typeof t && t && t.__esModule) return t; var n = Object.create(null); if (r.r(n), Object.defineProperty(n, "default", { enumerable: !0, value: t }), 2 & e && "string" != typeof t) for (var o in t) r.d(n, o, function(e) { return t[e] }.bind(null, o)); return n }, r.n = function(t) { var e = t && t.__esModule ? function() { return t.default } : function() { return t }; return r.d(e, "a", e), e }, r.o = function(t, e) { return Object.prototype.hasOwnProperty.call(t, e) }, r.p = "", r(r.s = 2) }([function(t, e) { var r = "undefined" != typeof crypto && crypto.getRandomValues && crypto.getRandomValues.bind(crypto) || "undefined" != typeof msCrypto && "function" == typeof window.msCrypto.getRandomValues && msCrypto.getRandomValues.bind(msCrypto); if (r) { var n = new Uint8Array(16); t.exports = function() { return r(n), n } } else { var o = new Array(16); t.exports = function() { for (var t, e = 0; e < 16; e++)0 == (3 & e) && (t = 4294967296 * Math.random()), o[e] = t >>> ((3 & e) << 3) & 255; return o } } }, function(t, e) { for (var r = [], n = 0; n < 256; ++n)r[n] = (n + 256).toString(16).substr(1); t.exports = function(t, e) { var n = e || 0, o = r; return [o[t[n++]], o[t[n++]], o[t[n++]], o[t[n++]], "-", o[t[n++]], o[t[n++]], "-", o[t[n++]], o[t[n++]], "-", o[t[n++]], o[t[n++]], "-", o[t[n++]], o[t[n++]], o[t[n++]], o[t[n++]], o[t[n++]], o[t[n++]]].join("") } }, function(t, e, r) { "use strict"; var n = this && this.__awaiter || function(t, e, r, n) { return new (r || (r = Promise))(function(o, i) { function s(t) { try { a(n.next(t)) } catch (t) { i(t) } } function u(t) { try { a(n.throw(t)) } catch (t) { i(t) } } function a(t) { t.done ? o(t.value) : new r(function(e) { e(t.value) }).then(s, u) } a((n = n.apply(t, e || [])).next()) }) }, o = this && this.__generator || function(t, e) { var r, n, o, i, s = { label: 0, sent: function() { if (1 & o[0]) throw o[1]; return o[1] }, trys: [], ops: [] }; return i = { next: u(0), throw: u(1), return: u(2) }, "function" == typeof Symbol && (i[Symbol.iterator] = function() { return this }), i; function u(i) { return function(u) { return function(i) { if (r) throw new TypeError("Generator is already executing."); for (; s;)try { if (r = 1, n && (o = 2 & i[0] ? n.return : i[0] ? n.throw || ((o = n.return) && o.call(n), 0) : n.next) && !(o = o.call(n, i[1])).done) return o; switch (n = 0, o && (i = [2 & i[0], o.value]), i[0]) { case 0: case 1: o = i; break; case 4: return s.label++, { value: i[1], done: !1 }; case 5: s.label++, n = i[1], i = [0]; continue; case 7: i = s.ops.pop(), s.trys.pop(); continue; default: if (!(o = (o = s.trys).length > 0 && o[o.length - 1]) && (6 === i[0] || 2 === i[0])) { s = 0; continue } if (3 === i[0] && (!o || i[1] > o[0] && i[1] < o[3])) { s.label = i[1]; break } if (6 === i[0] && s.label < o[1]) { s.label = o[1], o = i; break } if (o && s.label < o[2]) { s.label = o[2], s.ops.push(i); break } o[2] && s.ops.pop(), s.trys.pop(); continue }i = e.call(t, s) } catch (t) { i = [6, t], n = 0 } finally { r = o = 0 } if (5 & i[0]) throw i[1]; return { value: i[0] ? i[1] : void 0, done: !0 } }([i, u]) } } }; Object.defineProperty(e, "__esModule", { value: !0 }); var i = r(3), s = "_counter_shards_", u = "FIRESTORE_COUNTER_SHARD_ID", a = function() { function t(t, e) { this.doc = t, this.field = e, this.db = null, this.shardId = "", this.shards = {}, this.notifyPromise = null, this.db = t.firestore, this.shardId = function(t) { var e = new RegExp("(?:^|; )" + encodeURIComponent(t) + "=([^;]*)").exec(document.cookie); if (e) return e[1]; var r = i.v4(), n = new Date; n.setTime(n.getTime() + 2592e6); var o = "; expires=" + n.toUTCString(); return document.cookie = encodeURIComponent(t) + "=" + r + o + "; path=/", r }(u); var r = t.collection(s); this.shards[t.path] = 0, this.shards[r.doc(this.shardId).path] = 0, this.shards[r.doc("\t" + this.shardId.substr(0, 4)).path] = 0, this.shards[r.doc("\t\t" + this.shardId.substr(0, 3)).path] = 0, this.shards[r.doc("\t\t\t" + this.shardId.substr(0, 2)).path] = 0, this.shards[r.doc("\t\t\t\t" + this.shardId.substr(0, 1)).path] = 0 } return t.prototype.get = function(t) { return n(this, void 0, void 0, function() { var e, r = this; return o(this, function(i) { switch (i.label) { case 0: return e = Object.keys(this.shards).map(function(e) { return n(r, void 0, void 0, function() { return o(this, function(r) { switch (r.label) { case 0: return [4, this.db.doc(e).get(t)]; case 1: return [2, r.sent().get(this.field) || 0] } }) }) }), [4, Promise.all(e)]; case 1: return [2, i.sent().reduce(function(t, e) { return t + e }, 0)] } }) }) }, t.prototype.onSnapshot = function(t) { var e = this; Object.keys(this.shards).forEach(function(r) { e.db.doc(r).onSnapshot(function(r) { e.shards[r.ref.path] = r.get(e.field) || 0, null === e.notifyPromise && (e.notifyPromise = function(t) { return n(this, void 0, void 0, function() { var e = this; return o(this, function(r) { return [2, new Promise(function(r) { return n(e, void 0, void 0, function() { var e = this; return o(this, function(i) { return setTimeout(function() { return n(e, void 0, void 0, function() { var e; return o(this, function(n) { return e = t(), r(e), [2] }) }) }, 0), [2] }) }) })] }) }) }(function() { var r = Object.values(e.shards).reduce(function(t, e) { return t + e }, 0); t({ exists: !0, data: function() { return r } }), e.notifyPromise = null })) }) }) }, t.prototype.incrementBy = function(t) { var e = firebase.firestore.FieldValue.increment(t), r = this.field.split(".").reverse().reduce(function(t, e) { var r; return (r = {})[e] = t, r }, e); return this.doc.collection(s).doc(this.shardId).set(r, { merge: !0 }) }, t.prototype.shard = function() { return this.doc.collection(s).doc(this.shardId) }, t }(); e.Counter = a }, function(t, e, r) { var n = r(4), o = r(5), i = o; i.v1 = n, i.v4 = o, t.exports = i }, function(t, e, r) { var n, o, i = r(0), s = r(1), u = 0, a = 0; t.exports = function(t, e, r) { var c = e && r || 0, f = e || [], d = (t = t || {}).node || n, l = void 0 !== t.clockseq ? t.clockseq : o; if (null == d || null == l) { var h = i(); null == d && (d = n = [1 | h[0], h[1], h[2], h[3], h[4], h[5]]), null == l && (l = o = 16383 & (h[6] << 8 | h[7])) } var p = void 0 !== t.msecs ? t.msecs : (new Date).getTime(), v = void 0 !== t.nsecs ? t.nsecs : a + 1, y = p - u + (v - a) / 1e4; if (y < 0 && void 0 === t.clockseq && (l = l + 1 & 16383), (y < 0 || p > u) && void 0 === t.nsecs && (v = 0), v >= 1e4) throw new Error("uuid.v1(): Can't create more than 10M uuids/sec"); u = p, a = v, o = l; var b = (1e4 * (268435455 & (p += 122192928e5)) + v) % 4294967296; f[c++] = b >>> 24 & 255, f[c++] = b >>> 16 & 255, f[c++] = b >>> 8 & 255, f[c++] = 255 & b; var m = p / 4294967296 * 1e4 & 268435455; f[c++] = m >>> 8 & 255, f[c++] = 255 & m, f[c++] = m >>> 24 & 15 | 16, f[c++] = m >>> 16 & 255, f[c++] = l >>> 8 | 128, f[c++] = 255 & l; for (var g = 0; g < 6; ++g)f[c + g] = d[g]; return e || s(f) } }, function(t, e, r) { var n = r(0), o = r(1); t.exports = function(t, e, r) { var i = e && r || 0; "string" == typeof t && (e = "binary" === t ? new Array(16) : null, t = null); var s = (t = t || {}).random || (t.rng || n)(); if (s[6] = 15 & s[6] | 64, s[8] = 63 & s[8] | 128, e) for (var u = 0; u < 16; ++u)e[i + u] = s[u]; return e || o(s) } }]);
    
    const question_ref = db.collection('questions').doc(context.params.question_id);
    
    // Initialize the sharded counter.
    var bookmarks_count = new sharded.Counter(question_ref, "bookmarks_count");

    bookmarks_count.incrementBy(1);
  });

The idea is when an user bookmarks a question. It creates a new document inside the subcollection bookmarks inside the corresponding question document. Then the function will detect the new document creation, and update the bookmark counter by one. I am trying to use the firebase's extension to do this. But somehow, the counter do not work when I deploy the function. What did I do wrong here?

Tumbling answered 18/8, 2020 at 16:48 Comment(7)
It is not clear if you are using the extension OR your own Cloud Function. Or did you integrate in your cloud Function some parts of the extension code? Can you pls clarify?Petrosal
I am integrating the extension into this cloud function, the compiled client code I just copied it from the extension descriptionTumbling
You are missing await. The firestore returns promise and you haven't handled it properly. const question_ref = await db.collection('questions').doc(context.params.question_id); - change this line @YugueChenKosse
and also you must return back a promise from a firebase function.Kosse
@SushanSapaliga just tried your solution, the error still persistsTumbling
@YugueChen Can you share the error you are getting while deploying it ?Kosse
I get ReferenceError: document is not defined, and the error happens at line where I copied the sample code for Firebase for the sharded function var sharded=function(t){var e={};function r(n){if(e[n])return e[n].exports;var o=e[n]={i:.......... I am thinking that the extension might not work in a node.js environmentTumbling
S
1

I would like to point out that Firebase Extensions are in Beta release [1]. There's no information on Distributed Counter doc [2] about Node.js; it mentions that its compiled minified JavaScript [3] and it says that users can use the provided client sample or your own client code to specify your document path and increment values [4].

So, when we check the Distributed counters doc [5] for node.js, users can increment and get a total of counts on Node.js.

[1] https://firebase.google.com/docs/extensions

[2] https://firebase.google.com/products/extensions/firestore-counter

[3] https://github.com/firebase/extensions/blob/master/firestore-counter/clients/web/dist/sharded-counter.js

[4] https://github.com/firebase/extensions/tree/master/firestore-counter

[5] https://firebase.google.com/docs/firestore/solutions/counters#node.js

Selfsuggestion answered 1/9, 2020 at 20:36 Comment(0)
T
1

If you're like us and only care about incrementing counters inside the cloud function and read them in the frontend, here's the simplified implementation we use (in Typescript). The Firestore extension takes care of the aggregation.

import * as uuid from 'uuid';
import { firestore } from 'firebase-admin/lib/firestore';

export class DistributedCounter {
  static async incrementBy(doc: firestore.DocumentReference, field: string, val: number): Promise<void> {
    const shardId = uuid.v4();
    const increment: any = firestore.FieldValue.increment(val);
    const update: { [key: string]: any } = field
      .split('.')
      .reverse()
      .reduce((value, name) => ({ [name]: value }), increment);
    await doc.collection('_counter_shards_').doc(shardId).set(update, { merge: true });
  }
}
Transceiver answered 26/3, 2021 at 0:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.