Files
memind/memory-v2-redis-streams.mjs
T
2026-07-03 09:46:03 +08:00

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),
};
},
};
}