redis-smq
Version:
A high-performance, reliable, and scalable message queue for Node.js.
256 lines • 9.1 kB
JavaScript
import { EventEmitter, OperationNotAllowedError, PanicError, PowerSwitch, } from 'redis-smq-common';
import { ConfigurationNotFoundError } from '../errors/configuration-not-found.error.js';
import { redisKeys } from '../common/redis/redis-keys/redis-keys.js';
import { parseConfig } from './parse-config.js';
import { defaultConfig } from './default-config.js';
import { withSharedPoolConnection } from '../common/redis/redis-connection-pool/with-shared-pool-connection.js';
import { ConfigurationUpdateError, UnexpectedScriptReplyError, } from '../errors/index.js';
import { ERedisScriptName } from '../common/redis/scripts.js';
var EConfigurationField;
(function (EConfigurationField) {
EConfigurationField["VERSION"] = "version";
EConfigurationField["DATA"] = "data";
})(EConfigurationField || (EConfigurationField = {}));
export class Configuration extends EventEmitter {
static instance = null;
static state = new PowerSwitch();
static initQueue = [];
operationLock = false;
operationQueue = [];
completionWaiters = [];
config;
constructor() {
super();
this.config = {
data: parseConfig(defaultConfig),
version: 0,
};
}
static initialize(cb) {
if (!this.validateInitState(cb))
return;
this.state.goingUp();
const instance = new Configuration();
instance.reload((err) => {
if (err instanceof ConfigurationNotFoundError) {
return instance.save(instance.config.data, (saveErr) => {
if (saveErr) {
this.state.rollback();
this.instance = null;
this.handleInitComplete(saveErr, cb);
}
else {
this.instance = instance;
this.state.commit();
this.handleInitComplete(null, cb);
}
});
}
if (err) {
this.state.rollback();
this.instance = null;
return this.handleInitComplete(err, cb);
}
this.instance = instance;
this.state.commit();
this.handleInitComplete(null, cb);
});
}
static getInstance() {
const state = this.state;
if (state.isDown()) {
throw new OperationNotAllowedError({
message: 'Configuration not initialized. Call initialize() first.',
});
}
if (state.isGoingUp()) {
throw new OperationNotAllowedError({
message: 'Configuration is initializing. Please wait.',
});
}
if (state.isGoingDown()) {
throw new OperationNotAllowedError({
message: 'Configuration is shutting down.',
});
}
if (!this.instance) {
throw new PanicError({
message: 'Configuration instance is null despite being initialized',
});
}
return this.instance;
}
static getConfig() {
return this.getInstance().getConfig().data;
}
static shutdown(cb) {
const state = this.state;
if (state.isDown())
return cb();
if (state.isGoingDown()) {
return cb(new OperationNotAllowedError({ message: 'Already shutting down' }));
}
if (state.isGoingUp()) {
return cb(new OperationNotAllowedError({
message: 'Cannot shutdown while initializing',
}));
}
state.goingDown();
if (this.instance) {
this.instance.waitForCompletion(() => {
this.instance = null;
state.commit();
cb();
});
}
else {
state.commit();
cb();
}
}
getConfig() {
return Object.freeze({
...this.config,
});
}
reload(cb) {
withSharedPoolConnection((client, done) => {
const key = redisKeys.getMainKeys().keyConfiguration;
client.hgetall(key, (err, result) => {
if (err)
return done(err);
const version = result?.[EConfigurationField.VERSION];
const data = result?.[EConfigurationField.DATA];
if (!version || !data) {
return done(new ConfigurationNotFoundError());
}
Object.assign(this.config.data, JSON.parse(data));
this.config.version = Number(version);
done(null, this.config);
});
}, cb);
}
save(config, cb) {
this.runExclusive((done) => {
withSharedPoolConnection((client, cb) => {
const key = redisKeys.getMainKeys().keyConfiguration;
const configData = JSON.stringify(config);
client.runScript(ERedisScriptName.SAVE_CONFIG, [key], [
EConfigurationField.VERSION,
EConfigurationField.DATA,
this.config.version,
configData,
], (err, reply) => {
if (err)
return cb(err);
if (typeof reply === 'number') {
Object.assign(this.config.data, config);
this.config.version = Number(reply);
this.emit('configuration.updated', this.config.data, this.config.version);
return cb();
}
if (reply === 'VERSION_MISMATCH') {
return this.reload((loadErr) => {
if (loadErr)
return cb(loadErr);
cb(new ConfigurationUpdateError({
message: 'Version mismatch during save',
}));
});
}
cb(new UnexpectedScriptReplyError({
message: 'Unexpected result from config save',
metadata: { reply },
}));
});
}, done);
}, cb);
}
updateConfigFromEvent(config, version, cb) {
this.runExclusive((done) => {
if (version <= this.config.version) {
return done(new ConfigurationUpdateError({
message: `Version mismatch: current=${this.config.version}, event=${version}`,
}));
}
Object.assign(this.config.data, config);
this.config.version = version;
this.emit('configuration.updated', this.config.data, this.config.version);
done();
}, cb);
}
static validateInitState(cb) {
const state = this.state;
if (state.isUp()) {
cb(new OperationNotAllowedError({
message: 'Configuration already initialized',
}));
return false;
}
if (state.isGoingUp()) {
this.initQueue.push(cb);
return false;
}
if (state.isGoingDown()) {
cb(new OperationNotAllowedError({
message: 'Cannot initialize while shutting down',
}));
return false;
}
return true;
}
static handleInitComplete(err, cb) {
const queued = this.initQueue.splice(0);
cb(err);
queued.forEach((qcb) => qcb(err));
}
runExclusive(fn, done) {
const execute = () => {
this.operationLock = true;
const release = (err) => {
try {
done(err);
}
finally {
this.operationLock = false;
this.notifyWaiters();
this.processNext();
}
};
try {
fn(release);
}
catch (err) {
release(err instanceof Error ? err : new ConfigurationUpdateError());
}
};
if (this.operationLock) {
this.operationQueue.push(execute);
}
else {
execute();
}
}
processNext() {
if (this.operationLock || this.operationQueue.length === 0)
return;
setImmediate(() => {
if (!this.operationLock) {
this.operationQueue.shift()?.();
}
});
}
notifyWaiters() {
if (!this.operationLock && this.operationQueue.length === 0) {
const waiters = this.completionWaiters.splice(0);
waiters.forEach((waiter) => waiter());
}
}
waitForCompletion(cb) {
if (!this.operationLock && this.operationQueue.length === 0) {
return cb();
}
this.completionWaiters.push(cb);
}
}
//# sourceMappingURL=configuration.js.map