diff --git a/bin/www.js b/bin/www.js index f3302eef..641ec8bc 100644 --- a/bin/www.js +++ b/bin/www.js @@ -7,20 +7,37 @@ import * as configFunctions from '../helpers/functions.config.js'; import exitHook from 'exit-hook'; import ntfyPublish from '@cityssm/ntfy-publish'; import Debug from 'debug'; -const debug = Debug('lot-occupancy-system:www'); +const debug = Debug(`lot-occupancy-system:www:${process.pid}`); const directoryName = dirname(fileURLToPath(import.meta.url)); const processCount = Math.min(configFunctions.getProperty('application.maximumProcesses'), os.cpus().length); -debug(`Primary pid: ${process.pid}`); +process.title = + configFunctions.getProperty('application.applicationName') + ' (Primary)'; +debug(`Primary pid: ${process.pid}`); +debug(`Primary title: ${process.title}`); debug(`Launching ${processCount} processes`); const clusterSettings = { exec: directoryName + '/wwwProcess.js' }; cluster.setupPrimary(clusterSettings); +const activeWorkers = new Map(); for (let index = 0; index < processCount; index += 1) { - cluster.fork(); + const worker = cluster.fork(); + activeWorkers.set(worker.process.pid, worker); } +cluster.on('message', (worker, message) => { + if (message?.messageType === 'clearCache') { + for (const [pid, worker] of activeWorkers.entries()) { + if (worker === undefined || pid === message.pid) { + continue; + } + debug('Relaying message to workers'); + worker.send(message); + } + } +}); cluster.on('exit', (worker, code, signal) => { debug(`Worker ${worker.process.pid.toString()} has been killed`); + activeWorkers.delete(worker.process.pid); debug('Starting another worker'); cluster.fork(); }); diff --git a/bin/www.ts b/bin/www.ts index 46bd256e..c62352fa 100644 --- a/bin/www.ts +++ b/bin/www.ts @@ -12,8 +12,10 @@ import exitHook from 'exit-hook' import ntfyPublish from '@cityssm/ntfy-publish' import type * as ntfyTypes from '@cityssm/ntfy-publish/types' +import type { WorkerMessage } from '../types/applicationTypes' + import Debug from 'debug' -const debug = Debug('lot-occupancy-system:www') +const debug = Debug(`lot-occupancy-system:www:${process.pid}`) const directoryName = dirname(fileURLToPath(import.meta.url)) @@ -22,7 +24,11 @@ const processCount = Math.min( os.cpus().length ) -debug(`Primary pid: ${process.pid}`) +process.title = + configFunctions.getProperty('application.applicationName') + ' (Primary)' + +debug(`Primary pid: ${process.pid}`) +debug(`Primary title: ${process.title}`) debug(`Launching ${processCount} processes`) const clusterSettings = { @@ -31,12 +37,30 @@ const clusterSettings = { cluster.setupPrimary(clusterSettings) +const activeWorkers = new Map() + for (let index = 0; index < processCount; index += 1) { - cluster.fork() + const worker = cluster.fork() + activeWorkers.set(worker.process.pid!, worker) } +cluster.on('message', (worker, message: WorkerMessage) => { + if (message?.messageType === 'clearCache') { + for (const [pid, worker] of activeWorkers.entries()) { + if (worker === undefined || pid === message.pid) { + continue + } + + debug('Relaying message to workers') + worker.send(message) + } + } +}) + cluster.on('exit', (worker, code, signal) => { debug(`Worker ${worker.process.pid!.toString()} has been killed`) + activeWorkers.delete(worker.process.pid!) + debug('Starting another worker') cluster.fork() }) diff --git a/bin/wwwProcess.js b/bin/wwwProcess.js index 6f22caf4..ece15f74 100644 --- a/bin/wwwProcess.js +++ b/bin/wwwProcess.js @@ -29,6 +29,7 @@ function onListening(server) { debug('HTTP Listening on ' + bind); } } +process.title = configFunctions.getProperty('application.applicationName') + ' (Worker)'; const httpPort = configFunctions.getProperty('application.httpPort'); const httpServer = http.createServer(app); httpServer.listen(httpPort); diff --git a/bin/wwwProcess.ts b/bin/wwwProcess.ts index 56ac5f72..b643beaa 100644 --- a/bin/wwwProcess.ts +++ b/bin/wwwProcess.ts @@ -58,6 +58,8 @@ function onListening(server: http.Server): void { * Initialize HTTP */ +process.title = configFunctions.getProperty('application.applicationName') + ' (Worker)' + const httpPort = configFunctions.getProperty('application.httpPort') const httpServer = http.createServer(app) diff --git a/docs/cemeteryWorkflowDeceased.md b/docs/cemeteryWorkflowDeceased.md new file mode 100644 index 00000000..9a6a87cc --- /dev/null +++ b/docs/cemeteryWorkflowDeceased.md @@ -0,0 +1,33 @@ +[Home](https://cityssm.github.io/lot-occupancy-system/) +• +[Help](https://cityssm.github.io/lot-occupancy-system/docs/) + +# Cemetery Management System Workflow - Newly Deceased + +_The following workflow describes a process that can be used when the Lot Occupancy System is used +as a Cemetery Management System._ + +## Step 1: Search for a Related Preneed Occupancy Record + +![Occupancy Search](images/lotOccupancySearch.png) + +If the deceased purchased preneed services, find them now. + +![Occupancy View](images/lotOccupancyView.png) + +It is important to note what services have been paid for, +and who is entitled to the services. + +## Step 2: Create the New Interment or Cremation Occupancy Record + +![Occupancy Edit - More Options](images/lotOccupancyEdit-moreOptions.png) + +If a preneed occupancy record exists, you can save time by copying the preneed record as a new record. + +If no preneed occupancy record exists, a new occupancy record should be created. + +## Step 3: Create a Work Order Associated with the Occupancy Record + +Ensure the necessary milestones are included. + +## Step 4: Complete the Work Order \ No newline at end of file diff --git a/helpers/functions.cache.d.ts b/helpers/functions.cache.d.ts index 3a4adbdc..17bd15a7 100644 --- a/helpers/functions.cache.d.ts +++ b/helpers/functions.cache.d.ts @@ -18,4 +18,4 @@ export declare function getWorkOrderTypeById(workOrderTypeId: number): Promise; export declare function getWorkOrderMilestoneTypeById(workOrderMilestoneTypeId: number): Promise; export declare function getWorkOrderMilestoneTypeByWorkOrderMilestoneType(workOrderMilestoneTypeString: string): Promise; -export declare function clearCacheByTableName(tableName: string): void; +export declare function clearCacheByTableName(tableName: string, relayMessage?: boolean): void; diff --git a/helpers/functions.cache.js b/helpers/functions.cache.js index 8c4022d4..fa7d3ec6 100644 --- a/helpers/functions.cache.js +++ b/helpers/functions.cache.js @@ -1,3 +1,4 @@ +import cluster from 'node:cluster'; import * as configFunctions from './functions.config.js'; import { getLotOccupantTypes as getLotOccupantTypesFromDatabase } from './lotOccupancyDB/getLotOccupantTypes.js'; import { getLotStatuses as getLotStatusesFromDatabase } from './lotOccupancyDB/getLotStatuses.js'; @@ -6,9 +7,8 @@ import { getOccupancyTypes as getOccupancyTypesFromDatabase } from './lotOccupan import { getOccupancyTypeFields as getOccupancyTypeFieldsFromDatabase } from './lotOccupancyDB/getOccupancyTypeFields.js'; import { getWorkOrderTypes as getWorkOrderTypesFromDatabase } from './lotOccupancyDB/getWorkOrderTypes.js'; import { getWorkOrderMilestoneTypes as getWorkOrderMilestoneTypesFromDatabase } from './lotOccupancyDB/getWorkOrderMilestoneTypes.js'; -import { getConfigTableMaxTimeMillis } from './lotOccupancyDB/getConfigTableMaxTimeMillis.js'; -import { setIntervalAsync, clearIntervalAsync } from 'set-interval-async'; -import { asyncExitHook } from 'exit-hook'; +import Debug from 'debug'; +const debug = Debug(`lot-occupancy-system:functions.cache:${process.pid}`); let lotOccupantTypes; export async function getLotOccupantTypes() { if (lotOccupantTypes === undefined) { @@ -163,7 +163,7 @@ export async function getWorkOrderMilestoneTypeByWorkOrderMilestoneType(workOrde function clearWorkOrderMilestoneTypesCache() { workOrderMilestoneTypes = undefined; } -export function clearCacheByTableName(tableName) { +export function clearCacheByTableName(tableName, relayMessage = true) { switch (tableName) { case 'LotOccupantTypes': { clearLotOccupantTypesCache(); @@ -193,26 +193,23 @@ export function clearCacheByTableName(tableName) { break; } } -} -function clearAllCaches() { - clearLotOccupantTypesCache(); - clearLotStatusesCache(); - clearLotTypesCache(); - clearOccupancyTypesCache(); - clearWorkOrderMilestoneTypesCache(); - clearWorkOrderTypesCache(); -} -let configTimeMillis = 0; -async function checkCacheIntegrity() { - const timeMillis = await getConfigTableMaxTimeMillis(); - if (timeMillis > configTimeMillis) { - configTimeMillis = timeMillis; - clearAllCaches(); + try { + if (relayMessage && cluster.isWorker) { + const workerMessage = { + messageType: 'clearCache', + tableName, + timeMillis: Date.now(), + pid: process.pid + }; + debug(`Sending clear cache from worker: ${tableName}`); + process.send(workerMessage); + } } + catch { } } -const cacheTimer = setIntervalAsync(checkCacheIntegrity, 10 * 60 * 1000); -asyncExitHook(async () => { - await clearIntervalAsync(cacheTimer); -}, { - minimumWait: 250 +process.on('message', (message) => { + if (message.messageType === 'clearCache' && message.pid !== process.pid) { + debug(`Clearing cache: ${message.tableName}`); + clearCacheByTableName(message.tableName, false); + } }); diff --git a/helpers/functions.cache.ts b/helpers/functions.cache.ts index 68ec1ef8..18e3256e 100644 --- a/helpers/functions.cache.ts +++ b/helpers/functions.cache.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/indent */ +import cluster from 'node:cluster' + import * as configFunctions from './functions.config.js' import { getLotOccupantTypes as getLotOccupantTypesFromDatabase } from './lotOccupancyDB/getLotOccupantTypes.js' @@ -15,12 +17,11 @@ import { getWorkOrderTypes as getWorkOrderTypesFromDatabase } from './lotOccupan import { getWorkOrderMilestoneTypes as getWorkOrderMilestoneTypesFromDatabase } from './lotOccupancyDB/getWorkOrderMilestoneTypes.js' -import { getConfigTableMaxTimeMillis } from './lotOccupancyDB/getConfigTableMaxTimeMillis.js' - -import { setIntervalAsync, clearIntervalAsync } from 'set-interval-async' -import { asyncExitHook } from 'exit-hook' - import type * as recordTypes from '../types/recordTypes' +import type { WorkerMessage } from '../types/applicationTypes' + +import Debug from 'debug' +const debug = Debug(`lot-occupancy-system:functions.cache:${process.pid}`) /* * Lot Occupant Types @@ -301,7 +302,10 @@ function clearWorkOrderMilestoneTypesCache(): void { workOrderMilestoneTypes = undefined } -export function clearCacheByTableName(tableName: string): void { +export function clearCacheByTableName( + tableName: string, + relayMessage = true +): void { switch (tableName) { case 'LotOccupantTypes': { clearLotOccupantTypesCache() @@ -336,39 +340,26 @@ export function clearCacheByTableName(tableName: string): void { break } } + + try { + if (relayMessage && cluster.isWorker) { + const workerMessage: WorkerMessage = { + messageType: 'clearCache', + tableName, + timeMillis: Date.now(), + pid: process.pid + } + + debug(`Sending clear cache from worker: ${tableName}`) + + process.send!(workerMessage) + } + } catch {} } -function clearAllCaches(): void { - clearLotOccupantTypesCache() - clearLotStatusesCache() - clearLotTypesCache() - clearOccupancyTypesCache() - clearWorkOrderMilestoneTypesCache() - clearWorkOrderTypesCache() -} - -/* - * Config Time Millis - */ - -let configTimeMillis = 0 - -async function checkCacheIntegrity(): Promise { - const timeMillis = await getConfigTableMaxTimeMillis() - - if (timeMillis > configTimeMillis) { - configTimeMillis = timeMillis - clearAllCaches() +process.on('message', (message: WorkerMessage) => { + if (message.messageType === 'clearCache' && message.pid !== process.pid) { + debug(`Clearing cache: ${message.tableName}`) + clearCacheByTableName(message.tableName, false) } -} - -const cacheTimer = setIntervalAsync(checkCacheIntegrity, 10 * 60 * 1000) - -asyncExitHook( - async () => { - await clearIntervalAsync(cacheTimer) - }, - { - minimumWait: 250 - } -) +}) diff --git a/helpers/lotOccupancyDB/getConfigTableMaxTimeMillis.d.ts b/helpers/lotOccupancyDB/getConfigTableMaxTimeMillis.d.ts deleted file mode 100644 index 9e597d15..00000000 --- a/helpers/lotOccupancyDB/getConfigTableMaxTimeMillis.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export declare function getConfigTableMaxTimeMillis(): Promise; -export default getConfigTableMaxTimeMillis; diff --git a/helpers/lotOccupancyDB/getConfigTableMaxTimeMillis.js b/helpers/lotOccupancyDB/getConfigTableMaxTimeMillis.js deleted file mode 100644 index 4c9f8c82..00000000 --- a/helpers/lotOccupancyDB/getConfigTableMaxTimeMillis.js +++ /dev/null @@ -1,34 +0,0 @@ -import { acquireConnection } from './pool.js'; -export async function getConfigTableMaxTimeMillis() { - const database = await acquireConnection(); - const result = database - .prepare(`select max(timeMillis) as timeMillis from ( - select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis - from LotOccupantTypes - UNION - select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis - from LotStatuses - UNION - select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis - from LotTypes - UNION - select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis - from OccupancyTypes - UNION - select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis - from OccupancyTypeFields - UNION - select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis - from OccupancyTypePrints - UNION - select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis - from WorkOrderTypes - UNION - select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis - from WorkOrderMilestoneTypes - )`) - .get(); - database.release(); - return result?.timeMillis ?? 0; -} -export default getConfigTableMaxTimeMillis; diff --git a/helpers/lotOccupancyDB/getConfigTableMaxTimeMillis.ts b/helpers/lotOccupancyDB/getConfigTableMaxTimeMillis.ts deleted file mode 100644 index d1dce156..00000000 --- a/helpers/lotOccupancyDB/getConfigTableMaxTimeMillis.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { acquireConnection } from './pool.js' - -export async function getConfigTableMaxTimeMillis(): Promise { - const database = await acquireConnection() - - const result = database - .prepare( - `select max(timeMillis) as timeMillis from ( - select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis - from LotOccupantTypes - UNION - select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis - from LotStatuses - UNION - select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis - from LotTypes - UNION - select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis - from OccupancyTypes - UNION - select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis - from OccupancyTypeFields - UNION - select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis - from OccupancyTypePrints - UNION - select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis - from WorkOrderTypes - UNION - select max(max(recordUpdate_timeMillis, ifnull(recordDelete_timeMillis,0))) as timeMillis - from WorkOrderMilestoneTypes - )` - ) - .get() - - database.release() - - return result?.timeMillis ?? 0 -} - -export default getConfigTableMaxTimeMillis diff --git a/package.json b/package.json index 69f52c12..0fb8f8ec 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "scripts": { - "start": "cross-env NODE_ENV=production node ./bin/www", - "dev:test": "cross-env NODE_ENV=dev DEBUG=lot-occupancy-system:*,dynamics-gp:* TEST_DATABASES=true nodemon ./bin/www.js", + "start": "cross-env NODE_ENV=production node ./bin/www.js", + "dev:test": "cross-env NODE_ENV=dev DEBUG=lot-occupancy-system:*,dynamics-gp:* TEST_DATABASES=true nodemon --inspect ./bin/www.js", "dev:test:process": "cross-env NODE_ENV=dev DEBUG=lot-occupancy-system:* TEST_DATABASES=true nodemon ./bin/wwwProcess.js", "dev:live": "cross-env NODE_ENV=dev DEBUG=lot-occupancy-system:* nodemon ./bin/www.js", "cy:open": "cypress open --config-file cypress.config.js", diff --git a/public-scss/style.scss b/public-scss/style.scss index ae04e4b9..68f9874e 100644 --- a/public-scss/style.scss +++ b/public-scss/style.scss @@ -1,6 +1,6 @@ @import '@cityssm/bulma-webapp-css/cityssm'; @import 'bulma/sass/utilities/derived-variables'; -@import 'bulma-calendar/src/sass/index'; +@import 'bulma-calendar/src/sass'; @import '@cityssm/fa-glow/fa-glow'; $white: #fff; diff --git a/types/applicationTypes.d.ts b/types/applicationTypes.d.ts new file mode 100644 index 00000000..a671e722 --- /dev/null +++ b/types/applicationTypes.d.ts @@ -0,0 +1,6 @@ +export interface WorkerMessage { + messageType: 'clearCache'; + tableName: string; + timeMillis: number; + pid: number; +} diff --git a/types/applicationTypes.ts b/types/applicationTypes.ts new file mode 100644 index 00000000..afd4bb37 --- /dev/null +++ b/types/applicationTypes.ts @@ -0,0 +1,6 @@ +export interface WorkerMessage { + messageType: 'clearCache' + tableName: string + timeMillis: number + pid: number +}