n8n
Version:
n8n Workflow Automation Tool
476 lines • 19.1 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DurableEventLog = void 0;
const api_types_1 = require("@n8n/api-types");
const backend_common_1 = require("@n8n/backend-common");
const db_1 = require("@n8n/db");
const di_1 = require("@n8n/di");
const durable_log_metrics_1 = require("./durable-log-metrics");
const instance_ai_event_log_repository_1 = require("../repositories/instance-ai-event-log.repository");
const EPHEMERAL_TYPES = api_types_1.INSTANCE_AI_EPHEMERAL_EVENT_TYPES;
const MAX_APPEND_ATTEMPTS = 5;
const IDLE_FLUSH_MS = 3_000;
function isFlushMarker(entry) {
return 'flushSignal' in entry;
}
function serializedBytes(events) {
return events.reduce((total, event) => total + Buffer.byteLength(JSON.stringify(event), 'utf8'), 0);
}
let DurableEventLog = class DurableEventLog {
constructor(logger, repo, metrics) {
this.logger = logger;
this.repo = repo;
this.metrics = metrics;
this.pendingByThread = new Map();
this.draining = new Map();
this.lastSeq = new Map();
this.buffers = new Map();
this.emitters = new Map();
this.lifecycles = new Map();
this.idleFlushTimers = new Map();
this.idleFlushMs = IDLE_FLUSH_MS;
this.logger = this.logger.scoped('instance-ai');
}
publish(threadId, event, emit) {
this.emitters.set(threadId, emit);
const pending = this.pendingByThread.get(threadId);
const entry = { event, enqueuedAt: Date.now() };
if (pending)
pending.push(entry);
else
this.pendingByThread.set(threadId, [entry]);
this.ensureDraining(threadId);
this.scheduleIdleFlush(threadId);
}
scheduleIdleFlush(threadId) {
const existing = this.idleFlushTimers.get(threadId);
if (existing)
clearTimeout(existing);
const timer = setTimeout(() => {
this.idleFlushTimers.delete(threadId);
void this.flush(threadId).catch((error) => {
this.logger.error('Instance AI event log idle flush failed', { threadId, error });
});
}, this.idleFlushMs);
timer.unref();
this.idleFlushTimers.set(threadId, timer);
}
async getEventsAfter(threadId, afterSeq) {
return await this.repo.getAfter(threadId, afterSeq);
}
async getEventsForRuns(threadId, runIds) {
return await this.repo.getForRuns(threadId, runIds);
}
getOpenSegments(threadId) {
const threadBuffers = this.buffers.get(threadId);
if (!threadBuffers)
return [];
const segments = [];
for (const [key, buffer] of threadBuffers) {
const separator = key.indexOf(':');
const runId = key.slice(0, separator);
const agentId = key.slice(separator + 1);
for (const kind of ['reasoning', 'text']) {
const parts = kind === 'text' ? buffer.text : buffer.reasoning;
const unemitted = kind === 'text' ? buffer.textUnemittedParts : buffer.reasoningUnemittedParts;
const emitted = unemitted > 0 ? parts.slice(0, parts.length - unemitted) : parts;
if (emitted.length === 0)
continue;
const responseId = kind === 'text' ? buffer.textResponseId : buffer.reasoningResponseId;
segments.push({ runId, agentId, kind, responseId, text: emitted.join('') });
}
}
return segments;
}
async getNextEventId(threadId) {
return (await this.repo.maxSeq(threadId)) + 1;
}
clearThread(threadId) {
this.resolvePendingFlushes(threadId);
this.pendingByThread.delete(threadId);
this.lastSeq.delete(threadId);
this.buffers.delete(threadId);
this.emitters.delete(threadId);
this.lifecycles.delete(threadId);
const timer = this.idleFlushTimers.get(threadId);
if (timer)
clearTimeout(timer);
this.idleFlushTimers.delete(threadId);
}
clear() {
for (const threadId of this.pendingByThread.keys())
this.resolvePendingFlushes(threadId);
this.pendingByThread.clear();
this.lastSeq.clear();
this.buffers.clear();
this.emitters.clear();
this.lifecycles.clear();
for (const timer of this.idleFlushTimers.values())
clearTimeout(timer);
this.idleFlushTimers.clear();
}
resolvePendingFlushes(threadId) {
const pending = this.pendingByThread.get(threadId);
if (!pending)
return;
for (const entry of pending) {
if (isFlushMarker(entry))
entry.flushSignal.resolve();
}
}
async flush(threadId) {
const hasBuffers = (this.buffers.get(threadId)?.size ?? 0) > 0;
if (!this.pendingByThread.has(threadId) && !this.draining.has(threadId) && !hasBuffers) {
return;
}
await new Promise((resolve) => {
const entry = { flushSignal: { resolve } };
const pending = this.pendingByThread.get(threadId);
if (pending)
pending.push(entry);
else
this.pendingByThread.set(threadId, [entry]);
this.ensureDraining(threadId);
});
}
async flushAll() {
const threadIds = new Set([
...this.draining.keys(),
...this.buffers.keys(),
...this.pendingByThread.keys(),
]);
for (const threadId of threadIds) {
try {
await this.flush(threadId);
}
catch (error) {
this.logger.error('Failed to flush Instance AI event log on shutdown', {
threadId,
error,
});
}
}
}
ensureDraining(threadId) {
if (this.draining.has(threadId))
return;
const drain = (async () => {
try {
let batch = this.takePending(threadId);
while (batch.length > 0) {
try {
await this.drainBatch(threadId, batch);
}
catch (error) {
this.logger.error('Instance AI event log drain failed for a batch', {
threadId,
error,
});
for (const entry of batch) {
if (isFlushMarker(entry))
entry.flushSignal.resolve();
}
}
batch = this.takePending(threadId);
}
}
finally {
this.draining.delete(threadId);
}
})();
this.draining.set(threadId, drain);
}
async drainBatch(threadId, batch) {
const lifecycle = this.currentLifecycle(threadId);
const flushSignals = [];
const settleFlushes = () => {
for (const signal of flushSignals)
signal.resolve();
};
const emit = this.emitters.get(threadId);
if (!emit) {
for (const entry of batch) {
if (isFlushMarker(entry))
entry.flushSignal.resolve();
}
return;
}
const toPersist = [];
const toEmit = [];
for (const entry of batch) {
if (isFlushMarker(entry)) {
for (const block of this.takeAllOpenBlocks(threadId)) {
toEmit.push({ event: block, persistIndex: toPersist.length, live: false });
toPersist.push(block);
}
flushSignals.push(entry.flushSignal);
continue;
}
const { event } = entry;
if (EPHEMERAL_TYPES.has(event.type)) {
for (const rolled of this.rollSegmentOnResponseChange(threadId, event)) {
toEmit.push({ event: rolled, persistIndex: toPersist.length, live: false });
toPersist.push(rolled);
}
this.bufferDelta(threadId, event);
toEmit.push({ event, live: true });
continue;
}
for (const block of this.flushBlocks(threadId, event)) {
toEmit.push({ event: block, persistIndex: toPersist.length, live: false });
toPersist.push(block);
}
toEmit.push({ event, persistIndex: toPersist.length, live: true });
toPersist.push(event);
}
let firstSeq;
if (toPersist.length > 0) {
firstSeq = await this.persistWithRetry(threadId, toPersist, lifecycle);
if (this.lifecycles.get(threadId) !== lifecycle) {
settleFlushes();
return;
}
const persistedAt = Date.now();
for (const entry of batch) {
if (!isFlushMarker(entry))
this.metrics.recordQueueLatency(persistedAt - entry.enqueuedAt);
}
}
for (const drained of toEmit) {
const id = drained.persistIndex !== undefined && firstSeq !== undefined
? firstSeq + drained.persistIndex
: undefined;
emit({ ...(id !== undefined ? { id } : {}), event: drained.event, live: drained.live });
}
this.markBufferedPartsEmitted(threadId);
settleFlushes();
}
currentLifecycle(threadId) {
let token = this.lifecycles.get(threadId);
if (!token) {
token = {};
this.lifecycles.set(threadId, token);
}
return token;
}
takeAllOpenBlocks(threadId) {
const threadBuffers = this.buffers.get(threadId);
if (!threadBuffers || threadBuffers.size === 0)
return [];
const blocks = [];
for (const [key, buffer] of threadBuffers) {
const separator = key.indexOf(':');
const runId = key.slice(0, separator);
const agentId = key.slice(separator + 1);
const reasoning = this.takeBlock('reasoning', runId, agentId, buffer);
if (reasoning)
blocks.push(reasoning);
const text = this.takeBlock('text', runId, agentId, buffer);
if (text)
blocks.push(text);
}
this.buffers.delete(threadId);
return blocks;
}
async persistWithRetry(threadId, events, lifecycle) {
let lastError;
for (let attempt = 1; attempt <= MAX_APPEND_ATTEMPTS; attempt++) {
if (this.lifecycles.get(threadId) !== lifecycle) {
this.logger.debug('Instance AI event log dropped a batch for a cleared thread', {
threadId,
events: events.length,
});
return undefined;
}
let firstSeq;
try {
firstSeq = (await this.currentSeq(threadId)) + 1;
const bytes = await this.repo.appendBatch(threadId, firstSeq, events);
this.lastSeq.set(threadId, firstSeq + events.length - 1);
this.metrics.recordDrainBatch(events.length, bytes);
return firstSeq;
}
catch (error) {
lastError = error;
if ((0, db_1.isUniqueConstraintError)(error)) {
this.lastSeq.delete(threadId);
this.metrics.recordAppendConflict(attempt);
this.logger.warn('Instance AI event log append conflict, retrying', {
threadId,
attempt,
error,
});
continue;
}
if (firstSeq !== undefined && (await this.didBatchCommit(threadId, firstSeq, events))) {
this.lastSeq.set(threadId, firstSeq + events.length - 1);
this.metrics.recordDrainBatch(events.length, serializedBytes(events));
return firstSeq;
}
this.lastSeq.delete(threadId);
this.logger.warn('Instance AI event log append failed, retrying', {
threadId,
attempt,
error,
});
}
}
this.metrics.recordAppendFailure(events.length);
this.logger.error('Instance AI event log append failed, dropping batch', {
threadId,
events: events.length,
error: lastError,
});
return undefined;
}
async didBatchCommit(threadId, firstSeq, events) {
try {
const payload = await this.repo.payloadAt(threadId, firstSeq);
return payload !== null && payload === JSON.stringify(events[0]);
}
catch {
return false;
}
}
bufferDelta(threadId, event) {
if (event.type !== 'text-delta' && event.type !== 'reasoning-delta')
return;
if (!event.responseId) {
this.logger.debug('Instance AI durable log buffered a delta without a responseId', {
threadId,
runId: event.runId,
agentId: event.agentId,
});
}
const buffer = this.getOrCreateBuffer(threadId, `${event.runId}:${event.agentId}`);
if (event.type === 'text-delta') {
buffer.text.push(event.payload.text);
buffer.textUnemittedParts++;
buffer.textResponseId = event.responseId ?? buffer.textResponseId;
}
else {
buffer.reasoning.push(event.payload.text);
buffer.reasoningUnemittedParts++;
buffer.reasoningResponseId = event.responseId ?? buffer.reasoningResponseId;
}
}
markBufferedPartsEmitted(threadId) {
const threadBuffers = this.buffers.get(threadId);
if (!threadBuffers)
return;
for (const buffer of threadBuffers.values()) {
buffer.textUnemittedParts = 0;
buffer.reasoningUnemittedParts = 0;
}
}
flushBlocks(threadId, fact) {
const threadBuffers = this.buffers.get(threadId);
if (!threadBuffers)
return [];
const keys = fact.type === 'run-finish'
? [...threadBuffers.keys()].filter((k) => k.startsWith(`${fact.runId}:`))
: [`${fact.runId}:${fact.agentId}`];
const flushed = [];
for (const key of keys) {
const buffer = threadBuffers.get(key);
if (!buffer)
continue;
threadBuffers.delete(key);
const agentId = key.slice(fact.runId.length + 1);
const reasoning = this.takeBlock('reasoning', fact.runId, agentId, buffer);
if (reasoning)
flushed.push(reasoning);
const text = this.takeBlock('text', fact.runId, agentId, buffer);
if (text)
flushed.push(text);
}
return flushed;
}
rollSegmentOnResponseChange(threadId, event) {
if (event.type !== 'text-delta' && event.type !== 'reasoning-delta')
return [];
const buffer = this.buffers.get(threadId)?.get(`${event.runId}:${event.agentId}`);
if (!buffer)
return [];
const kind = event.type === 'text-delta' ? 'text' : 'reasoning';
const openResponseId = kind === 'text' ? buffer.textResponseId : buffer.reasoningResponseId;
const hasContent = (kind === 'text' ? buffer.text : buffer.reasoning).length > 0;
if (!hasContent || openResponseId === event.responseId)
return [];
const block = this.takeBlock(kind, event.runId, event.agentId, buffer);
return block ? [block] : [];
}
takeBlock(kind, runId, agentId, buffer) {
const parts = kind === 'text' ? buffer.text : buffer.reasoning;
if (parts.length === 0)
return undefined;
const text = parts.join('');
if (kind === 'text') {
const responseId = buffer.textResponseId;
buffer.text = [];
buffer.textUnemittedParts = 0;
buffer.textResponseId = undefined;
return {
type: 'text-block',
runId,
agentId,
...(responseId ? { responseId } : {}),
payload: { text },
};
}
const responseId = buffer.reasoningResponseId;
buffer.reasoning = [];
buffer.reasoningUnemittedParts = 0;
buffer.reasoningResponseId = undefined;
return {
type: 'reasoning-block',
runId,
agentId,
...(responseId ? { responseId } : {}),
payload: { text },
};
}
async currentSeq(threadId) {
const cached = this.lastSeq.get(threadId);
if (cached !== undefined)
return cached;
const max = await this.repo.maxSeq(threadId);
this.lastSeq.set(threadId, max);
return max;
}
getOrCreateBuffer(threadId, key) {
let threadBuffers = this.buffers.get(threadId);
if (!threadBuffers) {
threadBuffers = new Map();
this.buffers.set(threadId, threadBuffers);
}
let buffer = threadBuffers.get(key);
if (!buffer) {
buffer = { text: [], reasoning: [], textUnemittedParts: 0, reasoningUnemittedParts: 0 };
threadBuffers.set(key, buffer);
}
return buffer;
}
takePending(threadId) {
const pending = this.pendingByThread.get(threadId);
if (!pending)
return [];
this.pendingByThread.delete(threadId);
return pending;
}
};
exports.DurableEventLog = DurableEventLog;
exports.DurableEventLog = DurableEventLog = __decorate([
(0, di_1.Service)(),
__metadata("design:paramtypes", [backend_common_1.Logger, instance_ai_event_log_repository_1.InstanceAiEventLogRepository, durable_log_metrics_1.DurableLogMetrics])
], DurableEventLog);
//# sourceMappingURL=durable-event-log.js.map