UNPKG

mongo-lead

Version:
153 lines 4.95 kB
import { Db } from 'mongodb'; import { EventEmitter } from 'events'; import crypto from 'crypto'; export default class Leader extends EventEmitter { id; db; options; paused; initiated; constructor(db, options = {}) { super(); this.options = { collectionName: 'leader', groupName: 'default', ttl: 1000, wait: 500, ...options, }; this.id = crypto.randomUUID(); this.db = db; this.paused = false; this.initiated = false; } async isLeader() { if (this.paused) return false; if (!this.initiated) { await this.start(); } const item = await this.getCollection().findOne({ 'leader-id': this.id }); return item != null && item['leader-id'] === this.id; } async elect() { if (this.paused) return; try { const collection = this.getCollection(); const exists = (await collection.countDocuments()) > 0; const result = exists ? null : await collection.findOneAndUpdate({ groupName: this.options.groupName, }, { $set: { 'leader-id': this.id, createdAt: new Date(), }, $setOnInsert: { groupName: this.options.groupName, }, }, { upsert: true, returnDocument: 'after', }); const isElected = result && result['leader-id'] === this.id; if (!isElected) { setTimeout(() => this.elect(), this.options.wait); } else { this.emit('elected'); setTimeout(() => this.renew(), Math.floor(this.options.ttl / 4)); } } catch (error) { console.error('Election error:', error); setTimeout(() => this.elect(), this.options.wait); } } async renew() { if (this.paused) return; try { const expiresAt = new Date(Date.now() - this.options.ttl); const result = await this.getCollection().findOneAndUpdate({ 'leader-id': this.id, groupName: this.options.groupName, createdAt: { $gt: expiresAt }, }, { $set: { createdAt: new Date(), }, }, { returnDocument: 'after', }); if (result) { setTimeout(() => this.renew(), Math.floor(this.options.ttl / 4)); } else { this.emit('revoked'); setTimeout(() => this.elect(), this.options.wait); } } catch (error) { console.error('Renewal error:', error); this.emit('revoked'); setTimeout(() => this.elect(), this.options.wait); } } pause() { if (!this.paused) this.paused = true; } async resume() { if (this.paused) { this.paused = false; await this.elect(); } } async start() { if (!this.initiated) { this.initiated = true; await this.initDatabase(); await this.elect(); } } async initDatabase() { await this.db.command({ ping: 1 }); if (this.options.ttl < 1000) { try { await this.db .admin() .command({ setParameter: 1, ttlMonitorSleepSecs: 1 }); } catch (err) { console.warn('Unable to set TTL monitor sleep time. This is not critical, but TTL precision may be reduced.', err); } } const collection = await this.createCollection(); const ttlSeconds = Math.max(Math.floor(this.options.ttl / 1000), 1); try { await collection.dropIndex('createdAt_1'); await collection.dropIndex('groupName_1_createdAt_1'); } catch (err) { } await collection.createIndex({ createdAt: 1 }, { expireAfterSeconds: ttlSeconds }); await collection.createIndex({ groupName: 1 }); } async createCollection() { const cursor = this.db.listCollections({ name: this.options.collectionName, }); const exists = await cursor.hasNext(); const collection = exists ? this.db.collection(this.options.collectionName) : await this.db.createCollection(this.options.collectionName); return collection; } getCollection() { return this.db.collection(this.options.collectionName); } } //# sourceMappingURL=index.js.map