Files
memind/session-stream-store.mjs
john cce56a4c6a
Memind CI / Test, build, and release guards (pull_request) Successful in 2m55s
fix(chat): translate session replay cursors
2026-07-17 16:49:31 +08:00

127 lines
3.4 KiB
JavaScript

import crypto from 'node:crypto';
function safeJsonParse(value, fallback = null) {
try {
return JSON.parse(value);
} catch {
return fallback;
}
}
function mapEventRow(row) {
if (!row) return null;
return {
id: row.id,
eventType: row.event_type,
payload: safeJsonParse(row.payload_json, null),
upstreamEventId: row.upstream_event_id ?? null,
createdAt: Number(row.created_at),
};
}
export function createSessionStreamStore({ pool }) {
if (!pool) {
throw new Error('createSessionStreamStore requires pool');
}
async function appendEvent({
userId,
sessionId,
payload,
id = crypto.randomUUID(),
upstreamEventId = null,
} = {}) {
if (!userId || !sessionId || !payload) {
throw new Error('appendEvent requires userId, sessionId, and payload');
}
const createdAt = Date.now();
const eventType = String(payload?.type ?? 'unknown').slice(0, 64);
await pool.query(
`INSERT INTO h5_session_stream_events
(id, user_id, agent_session_id, event_type, payload_json, upstream_event_id, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
id,
userId,
sessionId,
eventType,
JSON.stringify(payload),
upstreamEventId || null,
createdAt,
],
);
return {
id,
createdAt,
eventType,
payload,
};
}
async function listEventsForUser(userId, sessionId, { afterEventId = null, limit = 500 } = {}) {
if (!userId || !sessionId) return null;
let afterCreatedAt = null;
let cursorMiss = false;
let cursorEvent = null;
if (afterEventId) {
const [cursorRows] = await pool.query(
`SELECT id, event_type, payload_json, upstream_event_id, created_at
FROM h5_session_stream_events
WHERE id = ? AND agent_session_id = ? AND user_id = ?
LIMIT 1`,
[afterEventId, sessionId, userId],
);
if (!cursorRows[0]) {
cursorMiss = true;
} else {
cursorEvent = mapEventRow(cursorRows[0]);
afterCreatedAt = cursorEvent.createdAt;
}
}
const [rows] = await pool.query(
afterCreatedAt == null
? `SELECT id, event_type, payload_json, upstream_event_id, created_at
FROM h5_session_stream_events
WHERE agent_session_id = ? AND user_id = ?
ORDER BY created_at ASC, id ASC
LIMIT ?`
: `SELECT id, event_type, payload_json, upstream_event_id, created_at
FROM h5_session_stream_events
WHERE agent_session_id = ? AND user_id = ? AND created_at > ?
ORDER BY created_at ASC, id ASC
LIMIT ?`,
afterCreatedAt == null
? [sessionId, userId, limit]
: [sessionId, userId, afterCreatedAt, limit],
);
return {
events: rows.map(mapEventRow),
cursorMiss,
cursorEvent,
};
}
async function getLatestEventForUser(userId, sessionId) {
const [rows] = await pool.query(
`SELECT id, event_type, payload_json, upstream_event_id, created_at
FROM h5_session_stream_events
WHERE agent_session_id = ? AND user_id = ?
ORDER BY created_at DESC, id DESC
LIMIT 1`,
[sessionId, userId],
);
const row = rows[0];
if (!row) return null;
return mapEventRow(row);
}
return {
appendEvent,
listEventsForUser,
getLatestEventForUser,
};
}