UNPKG

@intuitionrobotics/push-pub-sub

Version:
194 lines 9.91 kB
import { __stringify, batchAction, batchActionParallel, compare, currentTimeMillies, Day, filterDuplicates, generateHex, Hour, Module } from "@intuitionrobotics/ts-common"; import { FirebaseModule, FirestoreCollection, FirestoreTransaction, PushMessagesWrapper } from "@intuitionrobotics/firebase/backend"; // noinspection TypeScriptPreferShortImport import {} from "../../index.js"; import { dispatch_queryRequestInfo } from "@intuitionrobotics/thunderstorm/backend"; export class PushPubSubModule_Class extends Module { constructor() { super("PushPubSubModule"); } // All resources resolve lazily on first use — a container that never pushes // never creates the admin session, the collections or the messaging client. _pushSessions; _pushKeys; _notifications; _messaging; get session() { return FirebaseModule.createAdminSession(); } get pushSessions() { return this._pushSessions ??= this.session.getFirestore().getCollection("push-sessions", ["pushSessionId"]); } get pushKeys() { return this._pushKeys ??= this.session.getFirestore().getCollection("push-keys"); } get notifications() { return this._notifications ??= this.session.getFirestore().getCollection("notifications", ["_id"]); } get messaging() { return this._messaging ??= this.session.getMessaging(); } async register(body, request) { const resp = await dispatch_queryRequestInfo.dispatchModuleAsync(request); const userId = resp.find(e => e.key === "AccountsModule")?.data?._id || resp.find(e => e.key === "RemoteProxy")?.data; // if (!userId) // throw new ImplementationMissingException("Missing user from accounts Module"); const session = { firebaseToken: body.firebaseToken, pushSessionId: body.pushSessionId, timestamp: currentTimeMillies() }; if (body.language) session.language = body.language; if (userId) session.userId = userId; const subscriptions = body.subscriptions.map((s) => { const sub = { pushSessionId: body.pushSessionId, pushKey: s.pushKey }; if (s.props) sub.props = s.props; return sub; }); return this.pushSessions.runInTransaction(async (transaction) => { const pushKeys = subscriptions.map(_sub => _sub.pushKey); let subscriptionNotifications = pushKeys.length !== 0 ? await batchAction(pushKeys, 10, async (elements) => transaction.query(this.notifications, { where: { pushKey: { $in: elements } } })) : []; if (subscriptionNotifications.length > 0) subscriptionNotifications = subscriptionNotifications.filter(_notification => { const x = subscriptions.find(_sub => _sub.pushKey === _notification.pushKey)?.props; return compare(x, _notification.props) || _notification.userId; }); const userNotifications = userId ? await transaction.query(this.notifications, { where: { userId } }) : []; const notifications = userNotifications.concat(subscriptionNotifications); const writePush = await transaction.upsert_Read(this.pushSessions, session); const write = await transaction.delete_Read(this.pushKeys, { where: { pushSessionId: body.pushSessionId } }); await transaction.insertAll(this.pushKeys, subscriptions); await Promise.all([write(), writePush()]); return filterDuplicates(notifications); }); } async pushToKey(key, props, data, persistent = false, transaction) { const processor = async (_transaction) => { console.log("i am pushing to key...", key, props, data); let docs = await _transaction.query(this.pushKeys, { where: { pushKey: key } }); if (props) docs = docs.filter(doc => !doc.props || compare(doc.props, props)); const notification = this.buildNotification(key, persistent, data, props); // If we need to do read and write for a transaction move this to a callback and rename to pushToKey_Read if (persistent) { await this.notifications.insertAll([notification]); } if (docs.length === 0) return; const sessionsIds = docs.map(d => d.pushSessionId); // I get the tokens relative to those sessions (query) const sessions = await batchAction(sessionsIds, 10, async (elements) => _transaction.query(this.pushSessions, { where: { pushSessionId: { $in: elements } } })); const _messages = docs.reduce((carry, db_pushKey) => { const session = sessions.find(s => s.pushSessionId === db_pushKey.pushSessionId); if (!session) return carry; carry[session.firebaseToken] = [notification]; return carry; }, {}); const resp = await this.sendMessage(persistent, _messages); if (!resp) return this.logInfo('No messages to send. Empty subscriptions'); const { response, messages } = resp; this.logInfo(`${response.successCount} sent, ${response.failureCount} failed`, "messages", messages); // return this.cleanUp(response, messages); }; if (transaction) return processor(transaction); return this.pushKeys.runInTransaction(processor); } async pushToUser(user, key, props, data, persistent = false) { console.log("i am pushing to user...", user, props); const notification = this.buildNotification(key, persistent, data, props, user); const notifications = []; if (persistent) { notifications.push(notification); await this.notifications.insertAll(notifications); } const docs = await this.pushSessions.query({ where: { userId: user } }); if (docs.length === 0) return; const sessionsIds = docs.map(d => d.pushSessionId); const sessions = await batchAction(sessionsIds, 10, async (elements) => this.pushSessions.query({ where: { pushSessionId: { $in: elements } } })); const _messages = docs.reduce((carry, db_pushKey) => { const session = sessions.find(s => s.pushSessionId === db_pushKey.pushSessionId); if (!session) return carry; carry[session.firebaseToken] = [notification]; return carry; }, {}); await this.sendMessage(persistent, _messages); } buildNotification = (pushkey, persistent, data, props, user) => { const notification = { _id: generateHex(16), timestamp: currentTimeMillies(), read: false, pushKey: pushkey, persistent }; if (data) notification.data = data; if (props) notification.props = props; if (user) notification.userId = user; return notification; }; sendMessage = async (persistent, _messages) => { const messages = Object.keys(_messages).map(token => ({ token, data: { messages: __stringify(_messages[token]) } })); if (messages.length === 0) return; console.log("sending a message to \n" + Object.keys(_messages).join("\n")); const response = await this.messaging.sendAll(messages); console.log("and this is the response: " + response.responses.map(_response => _response.success)); return { response, messages }; }; readNotification = async (id, read) => { await this.notifications.patch({ _id: id, read }); }; scheduledCleanup = async () => { const sessionsCleanupTime = this.config?.sessionsCleanupTime || Hour; const notificationsCleanupTime = this.config?.notificationsCleanupTime || 7 * Day; const docs = await this.pushSessions.query({ where: { timestamp: { $lt: currentTimeMillies() - sessionsCleanupTime } } }); await Promise.all([ this.notifications.delete({ where: { timestamp: { $lt: currentTimeMillies() - notificationsCleanupTime } } }), this.cleanUpImpl(docs.map(d => d.firebaseToken)) ]); }; cleanUp = async (response, messages) => { this.logInfo(`${response.successCount} sent, ${response.failureCount} failed`); if (response.failureCount > 0) this.logWarning(response.responses.filter(r => r.error)); const toDelete = response.responses.reduce((carry, resp, i) => { if (!resp.success && messages[i]) carry.push(messages[i].token); return carry; }, []); //TODO: delete notifications for the user that are older than X return this.cleanUpImpl(toDelete); }; async cleanUpImpl(_toDelete) { if (_toDelete.length === 0) return; const toDelete = filterDuplicates(_toDelete); const _sessions = await batchActionParallel(toDelete, 10, async (elements) => this.pushSessions.query({ where: { firebaseToken: { $in: elements } } })); const sessions = filterDuplicates(_sessions.map(s => s.pushSessionId)); const async = [ batchActionParallel(toDelete, 10, async (elements) => this.pushSessions.delete({ where: { firebaseToken: { $in: elements } } })), batchActionParallel(sessions, 10, async (elements) => this.pushKeys.delete({ where: { pushSessionId: { $in: elements } } })) ]; await Promise.all(async); } } export const PushPubSubModule = new PushPubSubModule_Class(); //# sourceMappingURL=PushPubSubModule.js.map