swipelime-client-node
Version:
swipelime-client-node is the official swipelime Node.js client library
478 lines (477 loc) • 21.5 kB
JavaScript
"use strict";
// Copyright (c) 2024 swipelime (https://swipelime.com)
// Use of this source code is governed by an MIT
// license that can be found in the LICENSE file.
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ServiceHandler = void 0;
const events_1 = require("events");
const lodash_1 = require("lodash");
const TaskEvent_1 = __importDefault(require("./models/TaskEvent"));
const TaskCommand_1 = __importDefault(require("./models/TaskCommand"));
const SystemAlert_1 = __importDefault(require("./models/SystemAlert"));
const types_1 = require("./types");
const utils_1 = require("./utils");
// eslint-disable-next-line import/prefer-default-export
class ServiceHandler {
/**
* The client instance used for communication.
*/
_client;
/**
* The DDP client instance used for subscribing to tasks.
*/
_ddpClient;
/**
* The ID of the tenant.
*/
_tenantId;
/**
* The event emitter for handling service handler events.
*/
_eventEmitter;
/**
* The subscription for tasks.
*/
_tasksSubscription;
/**
* The cache for storing tasks.
*/
_taskCache = new Map();
/**
* The latest tasks that have been received.
* This is used to store the tasks that have been received while processing the previous batch of tasks.
* We are only interested in the latest batch os tasks because that's the most up to date data.
*/
_latestTasks = null;
/**
* A flag to indicate if we are processing tasks.
* This is used to prevent multiple tasks processing at the same time.
*/
_isProcessingTasks = false;
/**
* The timeout for tasks in milliseconds.
* If a task is not processed within this time it will be deferred.
* The check interval is used to check if there are any tasks that have timed out.
*/
_taskTimeout = 60000; // 1 minute timeout for tasks
_checkInterval = 30000; // Check every 30 seconds
/**
* Gets the event emitter for handling service handler events.
*/
get emitter() {
return this._eventEmitter;
}
/**
* Gets the ID of the tenant.
*/
get tenantId() {
return this._tenantId;
}
/**
* Creates a new instance of the ServiceHandler class.
* @param client - The client instance used for communication.
* @param ddpClient - The DDP client instance used for subscribing to tasks.
* @param tenantId - The ID of the tenant.
* @throws {Error} If the tenant ID is not provided.
*/
constructor(client, ddpClient, _tenantId) {
if (!_tenantId) {
throw (0, utils_1.swipelimeError)('Tenant ID is required');
}
this._client = client;
this._ddpClient = ddpClient;
this._tenantId = _tenantId;
this._eventEmitter = new events_1.EventEmitter();
this.init();
this.startTaskCheckInterval();
}
async isReady() {
await this._tasksSubscription.ready();
return true;
}
// Check if there are any new tasks that need to be processed
processTasksFinishes() {
if (this._latestTasks) {
const latestTasks = this._latestTasks;
this._latestTasks = null;
return this.runTasksQueue(latestTasks); // Use debounce to start processing the next batch of tasks
}
}
async processTasks(newTasks) {
if (!newTasks || newTasks.length === 0) {
this._isProcessingTasks = false;
this._taskCache.clear();
return this.processTasksFinishes();
}
// Set the flag that we are processing tasks
this._isProcessingTasks = true;
const newTaskIds = new Set(newTasks.map((task) => task.id));
// Filter out tasks that have been removed
this._taskCache.forEach((task) => {
if (!newTaskIds.has(task.id)) {
this._taskCache.delete(task.id);
}
});
// Get the tasks that are new
const { tasksToEmit, commandsToRefuse, eventsToConfirm } = newTasks.reduce((acc, task) => {
if (!this._taskCache.has(task.id)) {
if (task instanceof TaskEvent_1.default && (!task.data.eventType || !types_1.eventTypes.includes(task.data.eventType))) {
acc.eventsToConfirm.push(task);
return acc;
}
if (task instanceof TaskCommand_1.default && (!task.data.commandType || !types_1.commandTypes.includes(task.data.commandType))) {
acc.commandsToRefuse.push(task);
return acc;
}
acc.tasksToEmit.push(task);
this._taskCache.set(task.id, task);
}
return acc;
}, { tasksToEmit: [], commandsToRefuse: [], eventsToConfirm: [] });
const afterProcessPromises = [];
// These will be automatically refused
if (commandsToRefuse.length) {
afterProcessPromises.push(this.refuseTasks(commandsToRefuse));
}
// These will be automatically confirmed
if (eventsToConfirm.length) {
afterProcessPromises.push(this.confirmTaskEvents(eventsToConfirm));
}
if (afterProcessPromises.length) {
await Promise.all(afterProcessPromises);
}
if (tasksToEmit.length) {
this._eventEmitter.emit('newTasks', tasksToEmit);
}
// Processing is done so we can set the flag to false
this._isProcessingTasks = false;
// After finish we need to check if there are any new tasks that need to be processed
return this.processTasksFinishes();
}
runTasksQueue = (0, lodash_1.debounce)((newTasks) => {
if (this._isProcessingTasks) {
this._latestTasks = newTasks;
}
else {
this.processTasks(newTasks);
}
}, 200);
async init() {
try {
this._tasksSubscription = this._ddpClient.subscribe(`api/v${this._client.apiVersion}/tasks`, this._tenantId);
const reactiveTasksCollection = this._ddpClient.collection(`${this._tenantId}/tasks`).reactive();
const reactiveTasksMap = reactiveTasksCollection.map((tasks) => this.taskMapFunction(tasks));
// We receive new tasks asynchronously so we need to process them in a queue
reactiveTasksMap.onChange((newTasks) => this.runTasksQueue(newTasks));
}
catch (error) {
if (error.errorType === 'Meteor.Error') {
this._client.emitter.emit('error', (0, utils_1.swipelimeConsoleError)(`ServiceHandler startSubscriptions error: ${error.message}${error.details ? ` ${error.details}` : ''}`));
}
else {
this._client.emitter.emit('error', (0, utils_1.swipelimeConsoleError)(`ServiceHandler startSubscriptions error: ${JSON.stringify(error)}`));
}
}
}
taskMapFunction(taskData) {
if (taskData.taskType === types_1.TaskType.event)
return new TaskEvent_1.default(taskData, this, Date.now());
if (taskData.taskType === types_1.TaskType.command)
return new TaskCommand_1.default(taskData, this, Date.now());
throw (0, utils_1.swipelimeError)('Unknown task type');
}
getTaskIdFromTask(task) {
if (!task)
throw (0, utils_1.swipelimeError)('Task is missing');
return typeof task === 'string' ? task : task.id;
}
checkOptionalIdValidity(idData) {
if (!idData.id && !idData.externalId)
throw (0, utils_1.swipelimeError)('id or externalId is required');
}
/**
* Starts the interval to check for long-running tasks.
* If a task is not processed within the timeout, it will be deferred.
*/
startTaskCheckInterval() {
setInterval(() => {
// We skip the check if the client is not connected
if (!this._client.isConnected) {
return;
}
const now = Date.now();
const tasksToDefer = [];
this._taskCache.forEach((task) => {
if (now - task.timestampReceived > this._taskTimeout) {
tasksToDefer.push(task);
}
});
if (tasksToDefer.length > 0) {
tasksToDefer.forEach((task) => this._taskCache.delete(task.id));
this.deferTasks(tasksToDefer);
this._eventEmitter.emit('systemAlert', new SystemAlert_1.default({ systemAlertType: types_1.SystemAlertType['long-running-tasks'], tasks: tasksToDefer }, new Date()));
}
}, this._checkInterval);
}
callTenantMethod(methodName, ...args) {
return this._client.callMethod(`api/v${this._client.apiVersion}/${methodName}`, this._tenantId, ...args);
}
confirmTaskEvents(tasks) {
const taskIds = tasks.map((task) => this.getTaskIdFromTask(task));
return this.callTenantMethod('markTasksAsProcessed', taskIds);
}
confirmTaskEvent(task) {
return this.confirmTaskEvents([task]);
}
/**
* Confirms a test command.
* The test command can be fired from the test suite in the integration settings in swipelime. When the test command received, this method has to be called to confirm it. It's for testing purposes only.
*
* @param task - The task event, task command, or task ID.
*/
confirmTestCommand(task) {
return this.callTenantMethod('confirmTestCommand', this.getTaskIdFromTask(task));
}
/**
* Refusing multiple tasks.
* @param tasks - The tasks to refuse but they can also be the IDs of the tasks.
*/
refuseTasks(tasks) {
const taskIds = tasks.map((task) => this.getTaskIdFromTask(task));
return this.callTenantMethod('refuseTasks', taskIds);
}
/**
* You can defer multiple tasks if you can't process them at the moment.
* If tasks stay unprocessed for a long time, they will be deferred automatically.
* @param tasks - The tasks to defer but they can also be the IDs of the tasks.
* Deferred tasks will be re-sent to you later unless they were deferred too many times.
*/
deferTasks(tasks) {
const taskIds = tasks.map((task) => this.getTaskIdFromTask(task));
return this.callTenantMethod('deferTasks', taskIds);
}
/**
* Test method to make a DDP error.
*/
async makeError() {
return this._client.callMethod(`api/v${this._client.apiVersion}/makeError`);
}
/**
* Pings the server.
* @returns A promise that resolves to 'pong'.
* This method can be used to check if the server is reachable.
* Valid login is required to use this method.
*/
ping() {
return this._client.callMethod(`api/v${this._client.apiVersion}/ping`);
}
/**
* @deprecated Use the markOrderItemsPaymentStatus method instead.
* It marks the payment request as done for a specific table.
* When the payment is done for a table this method has to be called so our system can reflect to that.
* @param tableIdData - The ID of the table.
* @returns A promise that resolves to true if it's successful.
* @throws {Error} If the table ID is not a valid ID or external ID.
*/
markPaymentDone(tableIdData) {
this.checkOptionalIdValidity(tableIdData);
return this.callTenantMethod('markPaymentChanged', tableIdData, true);
}
/**
* @deprecated Use the markOrderItemsPaymentStatus method instead.
* It marks the payment request as cancelled for a specific table.
* When the payment is cancelled for a table this method has to be called so our system can reflect to that.
* @param tableIdData - The ID of the table.
* @returns A promise that resolves to true if it's successful.
* @throws {Error} If the table ID is not a valid ID or external ID.
*/
markPaymentCancelled(tableIdData) {
this.checkOptionalIdValidity(tableIdData);
return this.callTenantMethod('markPaymentChanged', tableIdData, false);
}
/**
* Finish all table sessions on the table. Users will not be able to order anymore.
* When customers are leaving this method should be called to finish the table and lock their session so no more order can be made.
* @param tableIdData - The ID of the table.
* @returns A promise that resolves to true if it's successful.
* @throws {Error} If the table ID is not a valid ID or external ID.
*/
finishTable(tableIdData) {
this.checkOptionalIdValidity(tableIdData);
return this.callTenantMethod('finishTable', tableIdData);
}
/**
* Retrieves the ordered items for a specific table.
*
* @param tableIdData - The ID of the table.
* @returns A promise that resolves to the order event data.
*/
getOrderItems(tableIdData) {
this.checkOptionalIdValidity(tableIdData);
return this.callTenantMethod('getOrderItems', tableIdData);
}
/**
* Cancels the specified order items for a given table.
*
* @param tableIdData - The ID of the table.
* @param orderItemIds - An array of order item IDs to be cancelled.
* @returns A Promise that resolves to void.
*/
cancelOrderItems(tableIdData, orderItemIds) {
this.checkOptionalIdValidity(tableIdData);
if (!orderItemIds?.length)
throw (0, utils_1.swipelimeError)('cancelOrderItems method need valid orderItemIds');
return this.callTenantMethod('cancelOrderItems', tableIdData, orderItemIds);
}
/**
* Retrieves the universal menu elements from the server.
*
* @returns A promise that resolves to an array of UniversalMenuItem or UniversalMenuCategory objects.
*/
getUniversalMenuElements() {
return this.callTenantMethod('getUniversalMenuElements', this._tenantId);
}
/**
* Retrieves the universal menu items.
* @returns A promise that resolves to an array of UniversalMenuItem objects.
*/
getUniversalMenuItems() {
return this.callTenantMethod('getUniversalMenuElements', this._tenantId, 'item');
}
/**
* Retrieves the universal menu categories.
* @returns A promise that resolves to an array of UniversalMenuCategory objects.
*/
getUniversalMenuCategories() {
return this.callTenantMethod('getUniversalMenuElements', this._tenantId, 'category');
}
/**
* Retrieves all tables.
* @returns A promise that resolves to an array of NativeTable objects.
*/
getTables() {
return this.callTenantMethod('getTables');
}
/**
* Retrieves a table based on the provided table ID.
* @param tableIdData - The ID of the table.
* @returns A promise that resolves to the retrieved NativeTable object.
*/
async getTable(tableIdData) {
this.checkOptionalIdValidity(tableIdData);
return (await this.callTenantMethod('getTables', [tableIdData]))?.[0];
}
/**
* Upserts universal menu items.
*
* @param universalMenuItemsData - An array of partial UniversalMenuItemData objects.
* @param commandId - You can pass in the command id if this was a command from swipelime.
* @returns A promise that resolves to an object containing the number of items updated and the number of new items.
*/
upsertUniversalMenuItems(universalMenuItemsData, commandId) {
if (!universalMenuItemsData?.length)
throw (0, utils_1.swipelimeError)('upsertUniversalMenuItems method need valid universalMenuItemsData');
return this.callTenantMethod('upsertUniversalMenuItems', universalMenuItemsData, commandId);
}
/**
* Upserts tables.
*
* @param tableData - An array of partial NativeTable objects.
* @param commandId - You can pass in the command id if this was a command from swipelime.
* @returns A promise that resolves to an object containing the number of tables updated and the number of new items.
*/
upsertTables(tableData, commandId) {
if (!tableData?.length)
throw (0, utils_1.swipelimeError)('upsertUniversalMenuItems method need valid tableData');
return this.callTenantMethod('upsertTables', tableData, commandId);
}
/**
* Deletes menu elements (eg. items, categories) by their IDs.
* @param ids - An array of IdOption objects representing the IDs of the menu elements to delete.
* @returns A Promise that resolves to the number of menu elements deleted.
*/
deleteMenuElements(ids) {
if (!ids?.length)
throw (0, utils_1.swipelimeError)('deleteMenuElementsByIds method need valid ids');
return this.callTenantMethod('deleteMenuElementsByIds', ids);
}
/**
* Deletes tables by their IDs.
* @param ids - An array of IdOption objects representing the IDs of the tables to delete.
* @returns A Promise that resolves to the number of tables deleted.
*/
deleteTables(ids) {
if (!ids?.length)
throw (0, utils_1.swipelimeError)('deleteMenuElementsByIds method need valid ids');
return this.callTenantMethod('deleteTables', ids);
}
/**
* Adds a custom order item to the table. It only requires a label (which can be from any language) a quantity and a price.
* @param tableIdData - The ID of the table where the custom order item will be added.
* @param customOrderItem - The custom order item to be added.
* @returns A promise that resolves to an array of order item IDs either the original order item IDs or the newly generated ones.
*/
addCustomOrderItems(tableIdData, customOrderItem) {
this.checkOptionalIdValidity(tableIdData);
if (!customOrderItem?.length)
throw (0, utils_1.swipelimeError)('addCustomOrderItems method need valid customOrderItem');
return this.callTenantMethod('addCustomOrderItems', tableIdData, customOrderItem);
}
/**
* Changes the status of the order items.
* @param tableIdData - The ID of the table where the custom order item will be added.
* @param orderItemChanges - The order item changes eg. { orderItemId1: 'confirmed', orderItemId2: 'cancelled' }
*/
changeOrderItemsStatus(tableIdData, orderItemChanges) {
this.checkOptionalIdValidity(tableIdData);
if (!Object.keys(orderItemChanges)?.length)
throw (0, utils_1.swipelimeError)('changeOrderItemStatus method need valid orderItemChanges');
return this.callTenantMethod('changeOrderItemsStatus', tableIdData, orderItemChanges);
}
/**
* Confirms a confirm universal menu elements command.
* This command is fired when swipelime needs a confirmation that the elements are existing in your system.
*
* @param task - The task event, task command, or task ID.
* @param elementsConfirmation - The confirmation of the elements eg. { elementId1: true, elementId2: false }
*/
confirmUniversalMenuElementsCommand(task, elementsConfirmation) {
if (!elementsConfirmation || !Object.keys(elementsConfirmation).length)
throw (0, utils_1.swipelimeError)('confirmUniversalMenuElementsCommand method need valid elementsConfirmation');
return this.callTenantMethod('confirmUniversalMenuElements', this.getTaskIdFromTask(task), elementsConfirmation);
}
/**
* Marks the order items as paid or cancelled.
* This is a feedback for us that the order items are paid and we can mark them as paid in our system.
* @param tableIdData - The ID of the table where the order items are.
* @param orderItemIds - An array of order item IDs to be marked as paid.
* @param paymentStatus - The payment status to be set for the order items (paid or cancelled).
* @returns A promise that resolves to the payment ID.
*/
markOrderItemsPaymentStatus(tableIdData, orderItemIds, paymentStatus) {
this.checkOptionalIdValidity(tableIdData);
if (!orderItemIds.length)
throw (0, utils_1.swipelimeError)('markOrderItemsAsPaid method need valid orderItemIds');
if (paymentStatus !== 'paid' && paymentStatus !== 'cancelled')
throw (0, utils_1.swipelimeError)('markOrderItemsAsPaid method need valid paymentStatus');
return this.callTenantMethod('markOrderItemsPaymentStatus', tableIdData, orderItemIds, paymentStatus);
}
/**
* Cancels the payment that was coming from the order-items-payment-confirmed
* This can be used if the payment request was not successful and you want to cancel it.
* This will mark the order items as unpaid and the customers can pay again.
* @param tableIdData - The ID of the table where the payment was made.
* @param paymentId - The ID of the payment to be cancelled.
* @returns A promise that resolves to void.
*/
cancelPayment(tableIdData, paymentId) {
this.checkOptionalIdValidity(tableIdData);
if (!paymentId)
throw (0, utils_1.swipelimeError)('cancelPayment method need valid paymentId');
return this.callTenantMethod('cancelPayment', tableIdData, paymentId);
}
}
exports.ServiceHandler = ServiceHandler;