| 1 | // Browser mock uses the same immutable-page contract as the host. The cache |
| 2 | // contains fixture data only and is bounded across all mock list entry points. |
| 3 | const snapshots = new Map<string, { binding: string; rows: unknown[]; expires: number }>(); |
| 4 | let sequence = 0; |
| 5 | |
| 6 | export function releaseMockReadSnapshot(id: string): void { snapshots.delete(id); } |
| 7 | |
| 8 | export function mockReadSnapshotPage<T>(kind: string, query: unknown, cursor: string | undefined, limit: number | undefined, currentRows: T[]) { |
| 9 | const binding = JSON.stringify([kind, query]); |
| 10 | let id: string; |
| 11 | let offset = 0; |
| 12 | if (cursor) { |
| 13 | const match = /^mock-read-(\d+):(\d+)$/.exec(cursor); |
| 14 | if (!match) throw Object.assign(new Error("Read snapshot expired"), { code: "stale_cursor" }); |
| 15 | id = `mock-read-${match[1]}`; offset = Number(match[2]); |
| 16 | } else { |
| 17 | for (const [key, value] of snapshots) if (value.expires <= Date.now()) snapshots.delete(key); |
| 18 | if (snapshots.size >= 64) snapshots.delete(snapshots.keys().next().value!); |
| 19 | id = `mock-read-${++sequence}`; |
| 20 | snapshots.set(id, { binding, rows: structuredClone(currentRows), expires: Date.now() + 30 * 60_000 }); |
| 21 | } |
| 22 | const snapshot = snapshots.get(id); |
| 23 | if (!snapshot || snapshot.binding !== binding || snapshot.expires <= Date.now() || offset > snapshot.rows.length) throw Object.assign(new Error("Read snapshot expired"), { code: "stale_cursor" }); |
| 24 | const end = Math.min(snapshot.rows.length, offset + Math.min(200, Math.max(1, limit || 50))); |
| 25 | return { items: structuredClone(snapshot.rows.slice(offset, end)) as T[], nextCursor: end < snapshot.rows.length ? `${id}:${end}` : "", snapshotId: id, snapshotExpiresAt: snapshot.expires }; |
| 26 | } |
| 27 |