durabull
Version:
A durable workflow engine built on top of BullMQ and Redis
232 lines (229 loc) • 7.06 kB
JavaScript
"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;