UNPKG

durabull

Version:

A durable workflow engine built on top of BullMQ and Redis

232 lines (229 loc) 7.06 kB
"use strict"; /** * Storage facade for workflow persistence */ Object.defineProperty(exports, "__esModule", { value: true }); exports.closeStorage = exports.setStorage = exports.getStorage = exports.RedisStorage = void 0; const ioredis_1 = require("ioredis"); const serializers_1 = require("../serializers"); const global_1 = require("../config/global"); /** * Redis-based storage implementation */ class RedisStorage { constructor(redisUrl, serializerType) { this.serializer = (0, serializers_1.getSerializer)('json'); const url = redisUrl || 'redis://localhost:6379'; this.redis = new ioredis_1.Redis(url); if (serializerType) { this.serializer = (0, serializers_1.getSerializer)(serializerType); } } /** * Write workflow record */ async writeRecord(rec) { const key = this.getRecordKey(rec.id); const data = this.serializer.serialize(rec); await this.redis.set(key, data); } /** * Read workflow record */ async readRecord(id) { const key = this.getRecordKey(id); const data = await this.redis.get(key); if (!data) return null; return this.serializer.deserialize(data); } /** * Write complete history * Optimized to only update cursor if events are managed via appendEvent */ async writeHistory(id, hist) { const eventsKey = this.getHistoryEventsKey(id); const cursorKey = this.getHistoryCursorKey(id); // If initializing or clearing if (hist.events.length === 0) { await this.redis.del(eventsKey); await this.redis.set(cursorKey, 0); return; } // Update cursor await this.redis.set(cursorKey, hist.cursor); const data = hist.events.map(e => this.serializer.serialize(e)); // Use Lua script for atomic check-and-set to avoid race conditions const script = ` local key = KEYS[1] local new_len = tonumber(ARGV[1]) local current_len = redis.call('LLEN', key) if current_len == new_len then return 0 end redis.call('DEL', key) if new_len > 0 then redis.call('RPUSH', key, unpack(ARGV, 2)) end return 1 `; await this.redis.eval(script, 1, eventsKey, data.length, ...data); } /** * Read history */ async readHistory(id) { const eventsKey = this.getHistoryEventsKey(id); const cursorKey = this.getHistoryCursorKey(id); const [eventsData, cursorData] = await Promise.all([ this.redis.lrange(eventsKey, 0, -1), this.redis.get(cursorKey) ]); if (eventsData.length === 0 && !cursorData) { return null; } const events = eventsData.map(item => this.serializer.deserialize(item)); const cursor = cursorData ? parseInt(cursorData, 10) : 0; return { events, cursor }; } /** * Append event to history (optimized) */ async appendEvent(id, ev) { const key = this.getHistoryEventsKey(id); const data = this.serializer.serialize(ev); await this.redis.rpush(key, data); } /** * List all signals for a workflow */ async listSignals(id) { const key = this.getSignalsKey(id); const items = await this.redis.lrange(key, 0, -1); return items.map(item => this.serializer.deserialize(item)); } /** * Push signal to workflow queue */ async pushSignal(id, signal) { const key = this.getSignalsKey(id); const data = this.serializer.serialize(signal); await this.redis.lpush(key, data); } /** * Pop signal from workflow queue (FIFO) */ async popSignal(id) { const key = this.getSignalsKey(id); const data = await this.redis.rpop(key); if (!data) return null; return this.serializer.deserialize(data); } /** * Acquire a lock for workflow or activity */ async acquireLock(id, lockName, ttlSeconds) { const key = this.getLockKey(id, lockName); const result = await this.redis.set(key, '1', 'EX', ttlSeconds, 'NX'); return result === 'OK'; } /** * Release a lock */ async releaseLock(id, lockName) { const key = this.getLockKey(id, lockName); await this.redis.del(key); } /** * Refresh activity heartbeat */ async refreshHeartbeat(workflowId, activityId, ttlSeconds) { const key = this.getHeartbeatKey(workflowId, activityId); await this.redis.set(key, Date.now().toString(), 'EX', ttlSeconds); } /** * Check last heartbeat timestamp */ async checkHeartbeat(workflowId, activityId) { const key = this.getHeartbeatKey(workflowId, activityId); const data = await this.redis.get(key); return data ? parseInt(data, 10) : null; } /** * Add child workflow relationship */ async addChild(parentId, childId) { const key = this.getChildrenKey(parentId); await this.redis.sadd(key, childId); } /** * Get all children of a workflow */ async getChildren(parentId) { const key = this.getChildrenKey(parentId); return await this.redis.smembers(key); } /** * Close Redis connection */ async close() { await this.redis.quit(); } getHistoryEventsKey(id) { return `durabull:wf:${id}:history:events`; } getHistoryCursorKey(id) { return `durabull:wf:${id}:history:cursor`; } getRecordKey(id) { return `durabull:wf:${id}:record`; } getSignalsKey(id) { return `durabull:wf:${id}:signals`; } getLockKey(id, lockName) { return `durabull:wf:${id}:locks:${lockName}`; } getHeartbeatKey(workflowId, activityId) { return `durabull:wf:${workflowId}:act:${activityId}:hb`; } getChildrenKey(parentId) { return `durabull:wf:${parentId}:children`; } } exports.RedisStorage = RedisStorage; /** * Global storage instance */ let storageInstance = null; /** * Get or create storage instance */ function getStorage() { if (!storageInstance) { const instance = global_1.Durabull.getActive(); const redisUrl = instance?.getConfig().redisUrl || 'redis://localhost:6379'; const serializer = instance?.getConfig().serializer || 'json'; storageInstance = new RedisStorage(redisUrl, serializer); } return storageInstance; } exports.getStorage = getStorage; /** * Set custom storage implementation */ function setStorage(storage) { storageInstance = storage; } exports.setStorage = setStorage; /** * Close storage connection */ async function closeStorage() { if (storageInstance && storageInstance instanceof RedisStorage) { await storageInstance.close(); storageInstance = null; } } exports.closeStorage = closeStorage;