问题描述
我正在尝试在云功能中实现Firebase的分布式计数器扩展。这个想法是检测何时创建文档,然后在文档内部添加一个计数器。下面是我的云函数:
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,{ 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",value: t }),2 & e && "string" != typeof t) for (var o in t) r.d(n,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++]]].join("") } },r) { "use strict"; var n = this && this.__awaiter || function(t,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,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,{ 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,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,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,function() { var e = this; return o(this,function(r) { return [2,new Promise(function(r) { return n(e,function(i) { return setTimeout(function() { return n(e,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,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 },r) { var n = r(4),o = r(5),i = o; i.v1 = n,i.v4 = o,t.exports = i },r) { var n,i = r(0),s = r(1),u = 0,a = 0; t.exports = function(t,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) } },r) { var n = r(0),o = r(1); t.exports = function(t,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);
});
这个想法是当用户为问题添加书签时。它在相应问题文档的子集合bookmarks
内创建一个新文档。然后,该功能将检测到新创建的文档,并将书签计数器更新一个。我正在尝试使用Firebase的扩展名来执行此操作。但是以某种方式,当我部署该功能时,计数器不起作用。我在这里做错了什么?
解决方法
我想指出Firebase Extensions在Beta版本[1]中。关于Node.js的分布式计数器文档[2]上没有任何信息。它提到了它的已编译的精简JavaScript [3],并且说用户可以使用提供的客户端样本或您自己的客户端代码来指定您的文档路径和增量值[4]。
因此,当我们检查Node.js的Distributed counters文档[5]时,用户可以增加并获取Node.js上的总数。
[1] https://firebase.google.com/docs/extensions
[2] https://firebase.google.com/products/extensions/firestore-counter
[4] https://github.com/firebase/extensions/tree/master/firestore-counter
[5] https://firebase.google.com/docs/firestore/solutions/counters#node.js
,如果您和我们一样,只关心在云函数内部递增计数器并在前端读取它们,那么这里是我们使用的简化实现(在 Typescript 中)。 Firestore 扩展负责聚合。
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 });
}
}