@intuitionrobotics/push-pub-sub
Version:
194 lines • 9.91 kB
JavaScript
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