112 lines
3.3 KiB
JavaScript
112 lines
3.3 KiB
JavaScript
function normalizeUrl(value) {
|
|
const raw = String(value ?? '').trim();
|
|
if (!raw) return null;
|
|
try {
|
|
const url = new URL(raw);
|
|
if (!['redis:', 'rediss:'].includes(url.protocol)) {
|
|
throw new Error(`Unsupported protocol: ${url.protocol}`);
|
|
}
|
|
return raw;
|
|
} catch (err) {
|
|
throw new Error(`Invalid MEMORY_REDIS_STREAMS_URL: ${err instanceof Error ? err.message : err}`);
|
|
}
|
|
}
|
|
|
|
function normalizeStream(value) {
|
|
const raw = String(value ?? 'memind:memory-events').trim();
|
|
if (!/^[a-zA-Z0-9:_-]+$/.test(raw)) {
|
|
throw new Error(`Invalid MEMORY_REDIS_STREAMS_STREAM: ${raw}`);
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
function flattenFields(input = {}) {
|
|
return {
|
|
user_id: String(input.userId ?? ''),
|
|
session_id: String(input.sessionId ?? ''),
|
|
event_type: String(input.eventType ?? 'memory.write'),
|
|
messages_json: JSON.stringify(Array.isArray(input.messages) ? input.messages : []),
|
|
created_at: new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
export async function createRedisStreamsClient({
|
|
url,
|
|
stream = 'memind:memory-events',
|
|
importRedis = (specifier) => import(specifier),
|
|
} = {}) {
|
|
const resolvedUrl = normalizeUrl(url);
|
|
const resolvedStream = normalizeStream(stream);
|
|
if (!resolvedUrl) throw new Error('createRedisStreamsClient requires MEMORY_REDIS_STREAMS_URL');
|
|
const imported = await importRedis('redis');
|
|
const createClient = imported?.createClient ?? imported?.default?.createClient;
|
|
if (typeof createClient !== 'function') throw new Error('redis module does not export createClient');
|
|
const client = createClient({ url: resolvedUrl });
|
|
let connected = false;
|
|
|
|
async function ensureConnected() {
|
|
if (connected) return;
|
|
await client.connect();
|
|
connected = true;
|
|
}
|
|
|
|
return {
|
|
async write(input = {}) {
|
|
await ensureConnected();
|
|
await client.xAdd(resolvedStream, '*', flattenFields(input));
|
|
return { saved: 1, analyzed: 0, memories: 0 };
|
|
},
|
|
async close() {
|
|
if (!connected) return;
|
|
connected = false;
|
|
await client.quit?.();
|
|
},
|
|
};
|
|
}
|
|
|
|
export function createRedisStreamsMemoryBackend({
|
|
enabled = false,
|
|
url = null,
|
|
stream = 'memind:memory-events',
|
|
client = null,
|
|
unavailableReason = null,
|
|
} = {}) {
|
|
const resolvedUrl = normalizeUrl(url);
|
|
const resolvedStream = normalizeStream(stream);
|
|
const configured = Boolean(enabled && resolvedUrl);
|
|
const hasClient = Boolean(client?.write);
|
|
const wired = Boolean(configured && hasClient);
|
|
const reason = unavailableReason
|
|
?? (enabled
|
|
? (!resolvedUrl ? 'url_not_configured' : 'client_not_configured')
|
|
: 'not_configured');
|
|
|
|
return {
|
|
name: 'redis-streams',
|
|
category: 'behavior',
|
|
role: 'event-tracking',
|
|
flag: 'MEMORY_REDIS_STREAMS_ENABLED',
|
|
unavailableReason: reason,
|
|
url: resolvedUrl,
|
|
stream: resolvedStream,
|
|
|
|
isAvailable() {
|
|
return Boolean(wired);
|
|
},
|
|
|
|
getUnavailableReason() {
|
|
return this.isAvailable() ? null : reason;
|
|
},
|
|
|
|
async write(input = {}) {
|
|
if (!this.isAvailable()) return { saved: 0, analyzed: 0, memories: 0 };
|
|
const result = await client.write({ ...input, url: resolvedUrl, stream: resolvedStream });
|
|
return {
|
|
saved: Number(result?.saved ?? 0),
|
|
analyzed: Number(result?.analyzed ?? 0),
|
|
memories: Number(result?.memories ?? 0),
|
|
};
|
|
},
|
|
};
|
|
}
|