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?
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
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 });
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With