jsmdb
Version:
JSMumps Database Component
826 lines (627 loc) • 20.5 kB
JavaScript
/*
* JSMumps Database API
*
* jsmdb.js: implementation of the API
*
*
* Copyright (C) 2017, 2021 Coherent Logic Development LLC
*
* Author: John P. Willis <jpw@coherent-logic.com>
*
*/
const logger = require('jsmlog');
const worker = require('./worker');
const nodem = require('nodem');
const fs = require('fs');
const readline = require('readline');
const util = require('util');
const SynQueue = require('./synqueue');
function JSMDB(opts)
{
var self = this;
if (!(this instanceof JSMDB)) return new JSMDB();
this.locals = {};
this.exitNode = process.exit;
process.exit = this.shutdown.bind(this);
if(opts) {
var workerCount = opts.workerCount || 1;
this.logLevel = opts.logLevel || 3;
}
else {
var workerCount = 1;
this.logLevel = 3;
var opts = {};
}
this.logger = new logger.Logger({
logLevel: this.logLevel,
moduleName: "jsmdb (parent, pid " + process.pid + ")"
});
this.logger.debug("logLevel is " + this.logLevel);
this.logger.debug("workerCount is " + workerCount);
this.db = new nodem.Gtm();
var result = this.db.open(opts.nodemOptions || {});
if(!result.ok) {
this.logger.error("error opening nodem");
}
else {
this.logger.debug("opened M database with pid " + result.pid);
}
this.workers = [];
for(i = 0; i < workerCount; i++) {
this.workers.push(new worker.Worker(this));
}
this.logger.debug("workers.length is " + this.workers.length);
process.on('SIGINT', (code) => {
this.logger.info("user initiated parent process shutdown (SIGINT) with code", code);
self.shutdown();
});
return this;
}
JSMDB.prototype.reconfigureNodem = function(nodemOptions) {
this.db.configure(nodemOptions || {});
};
JSMDB.prototype.updateLocalSymbols = function (s) {
this.locals = s;
};
JSMDB.prototype.shutdown = function (exitCode) {
var self = this;
this.logger.info("closing master process database connection");
var result = this.db.close({resetTerminal: true});
if(result) {
this.logger.error("error closing master process database connection");
}
this.logger.info("sending CP_SHUTDOWN to " + this.workers.length + " worker processes");
for(i = 0; i < this.workers.length; i++) {
try {
this.workers[i].dispatch('CP_SHUTDOWN');
}
catch (ex) {
// do nothing
}
}
setTimeout(() => {
this.logger.info("sending SIGTERM to all remaining worker processes");
for(i = 0; i < this.workers.length; i++) {
this.workers[i].worker.kill();
}
this.logger.info("exiting");
self.exitNode(exitCode);
}, 5000);
};
JSMDB.prototype.nextFreeWorker = function () {
for(var index in this.workers) {
if(this.workers[index].free()) {
this.logger.debug("worker process index " + index + " is free");
return this.workers[index].reserve();
}
}
this.logger.debug("no free workers found; creating a new worker");
// no free worker found... create a new one
this.workers.push(new worker.Worker(this));
return this.workers[this.workers.length - 1];
};
JSMDB.prototype.dispatch = function (action, opts, callback) {
var w = this.nextFreeWorker();
this.logger.debug("dispatching " + action + " to pid " + w.worker.pid);
return w.dispatch(action, opts, callback);
};
JSMDB.prototype.getObject = function(global, subscripts, callback) {
this.dispatch('getObject', {global: global, subscripts: subscripts}, callback);
};
JSMDB.prototype.getObjectSync = function(global, subscripts) {
throw new Error("API not yet implemented");
};
JSMDB.prototype.setObject = function(global, subscripts, data, callback) {
this.dispatch('setObject', {global: global, subscripts: subscripts, data: data}, callback);
};
JSMDB.prototype.setObjectSync = function(global, subscripts, data) {
throw new Error("API not yet implemented");
};
JSMDB.prototype.get = function(opts) {
validateOpts(opts);
if(opts.callback && !opts.childProcess) {
return this.db.get(opts.global, ...(opts.subscripts || []), opts.onComplete);
}
else if(opts.childProcess) {
if(opts.callback) {
this.dispatch("get", {global: opts.global, subscripts: opts.subscripts || []}, opts.onComplete);
}
else { /* promise mode is default */
return new Promise((resolve, reject) => {
this.dispatch("get", {global: opts.global, subscripts: opts.subscripts || []},
function (error, result) {
if(error) reject(error);
resolve(result.data);
});
});
}
}
else if (opts.defer) {
return opts.deferQueue.enqueue({
operation: "get",
global: opts.global,
subscripts: opts.subscripts || []
});
}
else if (opts.synchronous) {
return this.getSync(opts.global, opts.subscripts);
}
else { /* promise mode is default */
return new Promise((resolve, reject) => {
this.db.get(opts.global, ...(opts.subscripts || []),
function (error, result) {
if(error) reject(error);
resolve(result);
});
});
}
};
JSMDB.prototype.getSync = function(global, subscripts) {
return this.db.get({global: global, subscripts: subscripts});
};
JSMDB.prototype.set = function(opts) {
validateOpts(opts);
if(opts.callback && !opts.childProcess) {
return this.db.set(opts.global, ...(opts.subscripts || []), opts.data, opts.onComplete);
}
else if(opts.childProcess) {
if(opts.callback) {
this.dispatch("set", {global: opts.global, subscripts: opts.subscripts || [], data: opts.data}, opts.onComplete);
}
else { /* promise mode is default */
return new Promise((resolve, reject) => {
this.dispatch("set", {global: opts.global, subscripts: opts.subscripts || [], data: opts.data},
function (error, result) {
if(error) reject(error);
resolve(result.data);
});
});
}
}
else if (opts.defer) {
return opts.deferQueue.enqueue({
operation: "set",
global: opts.global,
subscripts: opts.subscripts || [],
data: opts.data
});
}
else if (opts.synchronous) {
return this.setSync(opts.global, opts.subscripts, opts.data);
}
else { /* promise mode is default */
return new Promise((resolve, reject) => {
this.db.set(opts.global, ...(opts.subscripts || []), opts.data,
function (error, result) {
if(error) reject(error);
resolve(result);
});
});
}
};
JSMDB.prototype.setSync = function(global, subscripts, data) {
return this.db.set({global: global, subscripts: subscripts, data: data});
};
JSMDB.prototype.merge = function(opts) {
if(opts.callback && !opts.childProcess) {
return this.db.merge({
to: {
global: opts.to.global,
subscripts: opts.to.subscripts
},
from: {
global: opts.from.global,
subscripts: opts.from.subscripts
}
}, opts.onComplete);
}
else if(opts.childProcess) {
if(opts.callback) {
this.dispatch('merge', {
to: {
global: opts.to.global,
subscripts: opts.to.subscripts
},
from: {
global: opts.from.global,
subscripts: opts.from.subscripts
}
}, opts.onComplete);
}
else {
return new Promise((resolve, reject) => {
this.dispatch('merge', {
to: {
global: opts.to.global,
subscripts: opts.to.subscripts
},
from: {
global: opts.from.global,
subscripts: opts.from.subscripts
}
}, function (error, result) {
if(error) reject(error);
resolve(result.data);
});
});
}
}
else if (opts.defer) {
return opts.deferQueue.enqueue({
operation: "merge",
from: opts.from,
to: opts.to
});
}
else if (opts.synchronous) {
return this.mergeSync(opts.from.global, opts.from.subscripts, opts.to.global, opts.to.subscripts);
}
else { /* promise mode is default */
return new Promise((resolve, reject) => {
this.db.merge({
to: {
global: opts.to.global,
subscripts: opts.to.subscripts
},
from: {
global: opts.from.global,
subscripts: opts.from.subscripts
}
}, function (error, result) {
if(error) reject(error);
resolve(result);
});
});
}
};
JSMDB.prototype.mergeSync = function(fromGlobal, fromSubscripts, toGlobal, toSubscripts) {
return this.db.merge({
to: {
global: toGlobal,
subscripts: toSubscripts
},
from: {
global: fromGlobal,
subscripts: fromSubscripts
}
});
};
JSMDB.prototype.kill = function(opts) {
validateOpts(opts);
if(opts.callback && !opts.childProcess) {
return this.db.kill(opts.global, ...(opts.subscripts || []), opts.onComplete);
}
else if(opts.childProcess) {
if(opts.callback) {
this.dispatch('kill', {global: opts.global, subscripts: opts.subscripts || []}, opts.onComplete);
}
else { /* promise mode is default */
return new Promise((resolve, reject) => {
this.dispatch('kill', {global: opts.global, subscripts: opts.subscripts || []},
function (error, result) {
if(error) reject(error);
resolve(result.data);
});
});
}
}
else if(opts.defer) {
return opts.deferQueue.enqueue({
operation: "kill",
global: opts.global,
subscripts: opts.subscripts || [],
});
}
else if(opts.synchronous) {
return this.killSync(opts.global, opts.subscripts || []);
}
else { /* promise mode is default */
return new Promise((resolve, reject) => {
this.db.kill(opts.global, ...(opts.subscripts || []),
function (error, result) {
if(error) reject(error);
resolve(result);
});
});
}
};
JSMDB.prototype.killSync = function(global, subscripts) {
return this.db.kill({global: global, subscripts: subscripts});
};
JSMDB.prototype.lock = function(opts) {
validateOpts(opts);
if(opts.callback && !opts.childProcess) {
return this.db.lock({global: opts.global, subscripts: opts.subscripts || [], timeout: opts.timeout}, opts.onComplete);
}
else if(opts.childProcess) {
if(opts.callback) {
this.dispatch('lock', {global: opts.global, subscripts: opts.subscripts || [], timeout: opts.timeout}, opts.onComplete);
}
else { /* promise mode is default */
return new Promise((resolve, reject) => {
this.dispatch("lock", {global: opts.global, subscripts: opts.subscripts || [], timeout: opts.timeout},
function (error, result) {
if(error) reject(error);
resolve(result.data);
});
});
}
}
else if (opts.defer) {
return opts.deferQueue.enqueue({
operation: "lock",
global: opts.global,
subscripts: opts.subscripts || [],
timeout: opts.timeout
});
}
else if (opts.synchronous) {
return this.lockSync(opts.global, ...(opts.subscripts || []), opts.timeout);
}
else { /* promise mode is default */
return new Promise((resolve, reject) => {
this.db.lock(opts.global, ...(opts.subscripts || []),
function (error, result) {
if(error) reject(error);
resolve(result);
});
});
}
};
JSMDB.prototype.lockSync = function(global, subscripts, timeout) {
return this.db.lock({global: global, subscripts: subscripts, timeout: timeout});
};
JSMDB.prototype.unlock = function(opts) {
validateOpts(opts);
if(opts.callback && !opts.childProcess) {
return this.db.unlock(opts.global, ...(opts.subscripts || []), opts.onComplete);
}
else if(opts.childProcess) {
if(opts.callback) {
this.dispatch('unlock', {global: opts.global, subscripts: opts.subscripts || []}, opts.onComplete);
}
else { /* promise mode is default */
return new Promise((resolve, reject) => {
this.dispatch("unlock", {global: opts.global, subscripts: opts.subscripts || []},
function (error, result) {
if(error) reject(error);
resolve(result.data);
});
});
}
}
else if (opts.defer) {
return opts.deferQueue.enqueue({
operation: "unlock",
global: opts.global,
subscripts: opts.subscripts || [],
});
}
else if (opts.synchronous) {
return this.unlockSync(opts.global, opts.subscripts || []);
}
else { /* promise mode is default */
return new Promise((resolve, reject) => {
this.db.unlock(opts.global, ...(opts.subscripts || []),
function (error, result) {
if(error) reject(error);
resolve(result);
});
});
}
};
JSMDB.prototype.unlockSync = function(global, subscripts) {
return this.db.unlock({global: global, subscripts: subscripts});
};
JSMDB.prototype.data = function(opts) {
validateOpts(opts);
if(opts.callback && !opts.childProcess) {
return this.db.data(opts.global, ...(opts.subscripts || []), opts.onComplete);
}
else if(opts.childProcess) {
if(opts.callback) {
this.dispatch("data", {global: opts.global, subscripts: opts.subscripts || []}, opts.onComplete);
}
else {
return new Promise((resolve, reject) => {
this.dispatch("data", {global: opts.global, subscripts: opts.subscripts || []},
function (error, result) {
if(error) reject(error);
resolve(result.data);
});
});
}
}
else if(opts.defer) {
return opts.deferQueue.enqueue({
operation: "data",
global: opts.global,
subscripts: opts.subscripts || []
});
}
else if(opts.synchronous) {
return this.dataSync(opts.global, opts.subscripts);
}
else { /* promise mode is default */
return new Promise((resolve, reject) => {
this.db.data(opts.global, ...(opts.subscripts || []),
function (error, result) {
if(error) reject(error);
resolve(result);
});
});
}
};
JSMDB.prototype.dataSync = function(global, subscripts) {
return this.db.data({global: global, subscripts: subscripts});
};
JSMDB.prototype.order = function(opts) {
validateOpts(opts);
if(opts.callback && !opts.childProcess) {
return this.db.order(opts.global, ...(opts.subscripts || []), opts.onComplete);
}
else if(opts.childProcess) {
if(opts.callback) {
this.dispatch("order", {global: opts.global, subscripts: opts.subscripts || []}, opts.onComplete);
}
else { /* promise mode is default */
return new Promise((resolve, reject) => {
this.dispatch("order", {global: opts.global, subscripts: opts.subscripts || []},
function (error, result) {
if(error) reject(error);
resolve(result.data);
});
});
}
}
else if(opts.defer) {
return opts.deferQueue.enqueue({
operation: "order",
global: opts.global,
subscripts: opts.subscripts || []
});
}
else if (opts.synchronous) {
return this.orderSync(opts.global, ...(opts.subscripts || []));
}
else { /* promise mode is default */
return new Promise((resolve, reject) => {
this.db.order(opts.global, ...(opts.subscripts || []),
function (error, result) {
if(error) reject(error);
resolve(result);
});
});
}
};
JSMDB.prototype.orderSync = function(global, subscripts) {
return this.db.order({global: global, subscripts: subscripts});
};
JSMDB.prototype.query = function(opts) {
validateOpts(opts);
if(opts.callback && !opts.childProcess) {
return this.db.query(opts.global, ...(opts.subscripts || []), opts.onComplete);
}
else if(opts.childProcess) {
if(opts.callback) {
this.dispatch("query", {global: opts.global, subscripts: opts.subscripts || []}, opts.onComplete);
}
else { /* promise mode is default */
return new Promise((resolve, reject) => {
this.dispatch("query", {global: opts.global, subscripts: opts.subscripts || []},
function (error, result) {
if(error) reject(error);
resolve(result.data);
});
});
}
}
else if(opts.defer) {
return opts.deferQueue.enqueue({
operation: "query",
global: opts.global,
subscripts: opts.subscripts || []
});
}
else if(opts.synchronous) {
return this.querySync(opts.global, opts.subscripts || []);
}
else { /* promise mode is default */
return new Promise((resolve, reject) => {
this.db.query(opts.global, ...(opts.subscripts || []),
function (error, result) {
if(error) reject(error);
resolve(result);
});
});
}
};
JSMDB.prototype.querySync = function(global, subscripts) {
return this.db.query({global: global, subscripts: subscripts});
};
JSMDB.prototype.import = function(routine, async) {
if(!async) var async = false;
routine = routine.replace("%", "_");
var dirs = process.env.gtmroutines.split(" ");
var directories = [];
var num = 0;
for(var i = 0; i < dirs.length; i++) {
if(dirs[i] !== "" && dirs[i].match(/.*\(.*\).*/)) {
directories[num] = dirs[i].split("(")[1].split(")")[0];
num++;
}
}
var file = "";
for(var i in directories) {
this.logger.debug("routine(): scanning " + directories[i] + " for " + routine);
file = directories[i] + "/" + routine + ".m";
if(fs.existsSync(file)) {
this.logger.debug("routine(): found routine " + routine + " at " + file);
break;
}
}
this.logger.debug("routine(): parsing " + file);
var fd = fs.openSync(file, "r");
var lines = fs.readFileSync(fd, "utf-8").split("\n");
this.logger.debug("routine(): read " + lines.length + " lines from " + file);
var line = "";
var methods = {};
var hasParameterList = false;
var parameterList = "";
for(line in lines) {
if(lines[line].match(/^[%A-Za-z0-9]+/)) {
var label = lines[line].split(" ")[0];
this.logger.debug("routine(): found label " + label + " at line " + (line + 1));
if(label.indexOf("(") > -1) {
hasParameterList = true;
var trueLabel = label.split("(")[0];
parameterList = label.split("(")[1].split(")")[0];
}
else {
hasParameterList = false;
var trueLabel = label;
}
if(trueLabel.charAt(trueLabel.length - 1) === ":") continue;
methods[trueLabel] = buildFunction(routine, trueLabel, this, async);
}
}
return methods;
};
function buildFunction(routine, label, instance, async) {
if(async) {
return function () {
var args = Array.prototype.slice.call(arguments);
var func = label + "^" + routine;
var cb = args.pop();
instance.dispatch('function', {func: func, args: args}, cb);
};
}
else {
return function () {
var args = Array.prototype.slice.call(arguments);
var func = label + "^" + routine;
return instance.functionSync(func, args);
};
}
}
function validateOpts(opts) {
if(!opts) {
throw new Error("Must pass an 'opts' object.");
}
if(typeof opts !== "object") {
throw new Error("'opts' must be an object.");
}
}
JSMDB.prototype.function = function(func, args, callback) {
this.dispatch('function', {func: func, args: args}, callback);
};
JSMDB.prototype.functionSync = function(func, args) {
return this.db.function({function: func, arguments: args});
};
JSMDB.prototype.procedure = function(proc, args, callback) {
this.dispatch('procedure', {proc: proc, args: args}, callback);
};
JSMDB.prototype.procedureSync = function(proc, args) {
return this.db.procedure({procedure: proc, arguments: args});
};
module.exports = JSMDB;