@hotmeshio/hotmesh
Version:
Permanent-Memory Workflows & AI Agents
202 lines (201 loc) • 7.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkerService = void 0;
const key_1 = require("../../modules/key");
const utils_1 = require("../../modules/utils");
const factory_1 = require("../connector/factory");
const router_1 = require("../router");
const stream_1 = require("../../types/stream");
const enums_1 = require("../../modules/enums");
const factory_2 = require("../stream/factory");
const factory_3 = require("../sub/factory");
const factory_4 = require("../store/factory");
class WorkerService {
/**
* @private
*/
constructor() {
this.reporting = false;
}
/**
* @private
*/
static async init(namespace, appId, guid, config, logger) {
const services = [];
if (Array.isArray(config.workers)) {
for (const worker of config.workers) {
// Pass taskQueue from top-level config to worker for connection pooling
if (config.taskQueue) {
worker.taskQueue = config.taskQueue;
}
await factory_1.ConnectorService.initClients(worker);
const service = new WorkerService();
service.verifyWorkerFields(worker);
service.namespace = namespace;
service.appId = appId;
service.guid = guid;
service.callback = worker.callback;
service.topic = worker.topic;
service.config = config;
service.logger = logger;
await service.initStoreChannel(service, worker.store);
await service.initSubChannel(service, worker.sub, worker.pub ?? worker.store);
await service.subscribe.subscribe(key_1.KeyType.QUORUM, service.subscriptionHandler(), appId);
await service.subscribe.subscribe(key_1.KeyType.QUORUM, service.subscriptionHandler(), appId, service.topic);
await service.subscribe.subscribe(key_1.KeyType.QUORUM, service.subscriptionHandler(), appId, service.guid);
await service.initStreamChannel(service, worker.stream, worker.store);
service.router = await service.initRouter(worker, logger);
const key = service.store.mintKey(key_1.KeyType.STREAMS, {
appId: service.appId,
topic: worker.topic,
});
await service.router.consumeMessages(key, 'WORKER', service.guid, worker.callback);
service.inited = (0, utils_1.formatISODate)(new Date());
services.push(service);
}
}
return services;
}
/**
* @private
*/
verifyWorkerFields(worker) {
if (!(0, utils_1.identifyProvider)(worker.store) ||
!(0, utils_1.identifyProvider)(worker.stream) ||
!(0, utils_1.identifyProvider)(worker.sub) ||
!(worker.topic && worker.callback)) {
throw new Error('worker must include `store`, `stream`, and `sub` fields along with a callback function and topic.');
}
}
/**
* @private
*/
async initStoreChannel(service, store) {
service.store = await factory_4.StoreServiceFactory.init(store, service.namespace, service.appId, service.logger);
}
/**
* @private
*/
async initSubChannel(service, sub, store) {
service.subscribe = await factory_3.SubServiceFactory.init(sub, store, service.namespace, service.appId, service.guid, service.logger);
}
/**
* @private
*/
async initStreamChannel(service, stream, store) {
service.stream = await factory_2.StreamServiceFactory.init(stream, store, service.namespace, service.appId, service.logger);
}
/**
* @private
*/
async initRouter(worker, logger) {
const throttle = await this.store.getThrottleRate(worker.topic);
return new router_1.Router({
namespace: this.namespace,
appId: this.appId,
guid: this.guid,
role: stream_1.StreamRole.WORKER,
topic: worker.topic,
reclaimDelay: worker.reclaimDelay,
reclaimCount: worker.reclaimCount,
throttle,
}, this.stream, logger);
}
/**
* @private
*/
subscriptionHandler() {
const self = this;
return async (topic, message) => {
self.logger.debug('worker-event-received', { topic, type: message.type });
if (message.type === 'throttle') {
if (message.topic !== null) {
//undefined allows passthrough
self.throttle(message.throttle);
}
}
else if (message.type === 'ping') {
self.sayPong(self.appId, self.guid, message.originator, message.details);
}
else if (message.type === 'rollcall') {
if (message.topic !== null) {
//undefined allows passthrough
self.doRollCall(message);
}
}
};
}
/**
* A quorum-wide command to broadcaset system details.
*
*/
async doRollCall(message) {
let iteration = 0;
const max = !isNaN(message.max) ? message.max : enums_1.HMSH_QUORUM_ROLLCALL_CYCLES;
if (this.rollCallInterval)
clearTimeout(this.rollCallInterval);
const base = message.interval / 2;
const amount = base + Math.ceil(Math.random() * base);
do {
await (0, utils_1.sleepFor)(Math.ceil(Math.random() * 1000));
await this.sayPong(this.appId, this.guid, null, true, message.signature);
if (!message.interval)
return;
const { promise, timerId } = (0, utils_1.XSleepFor)(amount * 1000);
this.rollCallInterval = timerId;
await promise;
} while (this.rollCallInterval && iteration++ < max - 1);
}
cancelRollCall() {
if (this.rollCallInterval) {
clearTimeout(this.rollCallInterval);
delete this.rollCallInterval;
}
}
/**
* @private
*/
stop() {
this.cancelRollCall();
}
/**
* @private
*/
async sayPong(appId, guid, originator, details = false, signature = false) {
let profile;
if (details) {
const params = {
appId: this.appId,
topic: this.topic,
};
profile = {
engine_id: this.guid,
namespace: this.namespace,
app_id: this.appId,
worker_topic: this.topic,
stream: this.store.mintKey(key_1.KeyType.STREAMS, params),
counts: this.router?.counts,
timestamp: (0, utils_1.formatISODate)(new Date()),
inited: this.inited,
throttle: this.router?.throttle,
reclaimDelay: this.router?.reclaimDelay,
reclaimCount: this.router?.reclaimCount,
system: await (0, utils_1.getSystemHealth)(),
signature: signature ? this.callback.toString() : undefined,
};
}
this.subscribe.publish(key_1.KeyType.QUORUM, {
type: 'pong',
guid,
originator,
profile,
}, appId);
}
/**
* @private
*/
async throttle(delayInMillis) {
this.router?.setThrottle(delayInMillis);
}
}
exports.WorkerService = WorkerService;