feat(sessions): per-user sessions and progress
This commit is contained in:
@@ -1,72 +1,76 @@
|
|||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
import { makeTestDb } from '../tests/dbHelper.js';
|
import { makeTestDb, createUserDirect } from '../tests/dbHelper.js';
|
||||||
import { createLesson } from './lessons.js';
|
import { createLesson } from './lessons.js';
|
||||||
import { createCard } from './cards.js';
|
import { createCard } from './cards.js';
|
||||||
import { startSession, getNextItem, recordAttempt, endSession, getActiveSession } from './sessions.js';
|
import { startSession, getNextItem, recordAttempt, endSession, getActiveSession } from './sessions.js';
|
||||||
|
|
||||||
let env: ReturnType<typeof makeTestDb>;
|
let env: ReturnType<typeof makeTestDb>;
|
||||||
beforeEach(() => { env = makeTestDb(); });
|
let owner: Awaited<ReturnType<typeof createUserDirect>>;
|
||||||
|
beforeEach(async () => {
|
||||||
|
env = makeTestDb();
|
||||||
|
owner = await createUserDirect(env.db, { email: 'owner@example.com' });
|
||||||
|
});
|
||||||
|
|
||||||
async function seedLesson(name = 'L', cards = 3, bidi = false) {
|
async function seedLesson(name = 'L', cards = 3, bidi = false) {
|
||||||
const lesson = await createLesson(env.db, { name, bidirectional: bidi });
|
const lesson = await createLesson(env.db, owner.id, { name, bidirectional: bidi });
|
||||||
for (let i = 0; i < cards; i++) {
|
for (let i = 0; i < cards; i++) {
|
||||||
await createCard(env.db, lesson.id, { question: `q${i}`, answer: `a${i}` });
|
await createCard(env.db, owner.id, lesson.id, { question: `q${i}`, answer: `a${i}` });
|
||||||
}
|
}
|
||||||
return lesson;
|
return lesson;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('session engine', () => {
|
describe('session engine', () => {
|
||||||
it('starts a session and queues all cards from lesson + descendants', async () => {
|
it('starts a session and queues all cards from lesson + descendants', async () => {
|
||||||
const root = await createLesson(env.db, { name: 'R' });
|
const root = await createLesson(env.db, owner.id, { name: 'R' });
|
||||||
const child = await createLesson(env.db, { name: 'C', parentId: root.id });
|
const child = await createLesson(env.db, owner.id, { name: 'C', parentId: root.id });
|
||||||
await createCard(env.db, root.id, { question: 'r1', answer: 'a' });
|
await createCard(env.db, owner.id, root.id, { question: 'r1', answer: 'a' });
|
||||||
await createCard(env.db, child.id, { question: 'c1', answer: 'a' });
|
await createCard(env.db, owner.id, child.id, { question: 'c1', answer: 'a' });
|
||||||
const s = await startSession(env.db, { lessonId: root.id, shuffle: false });
|
const s = await startSession(env.db, owner.id, { lessonId: root.id, shuffle: false });
|
||||||
expect(s.queue).toHaveLength(2);
|
expect(s.queue).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns items in queue order and marks done when exhausted', async () => {
|
it('returns items in queue order and marks done when exhausted', async () => {
|
||||||
const l = await seedLesson('L', 2);
|
const l = await seedLesson('L', 2);
|
||||||
const s = await startSession(env.db, { lessonId: l.id, shuffle: false });
|
const s = await startSession(env.db, owner.id, { lessonId: l.id, shuffle: false });
|
||||||
const a = await getNextItem(env.db, s.session.id);
|
const a = await getNextItem(env.db, owner.id, s.session.id);
|
||||||
expect(a).not.toBeNull();
|
expect(a).not.toBeNull();
|
||||||
await recordAttempt(env.db, s.session.id, { cardId: a!.cardId, direction: 'forward', result: 'correct' });
|
await recordAttempt(env.db, owner.id, s.session.id, { cardId: a!.cardId, direction: 'forward', result: 'correct' });
|
||||||
const b = await getNextItem(env.db, s.session.id);
|
const b = await getNextItem(env.db, owner.id, s.session.id);
|
||||||
expect(b).not.toBeNull();
|
expect(b).not.toBeNull();
|
||||||
await recordAttempt(env.db, s.session.id, { cardId: b!.cardId, direction: 'forward', result: 'correct' });
|
await recordAttempt(env.db, owner.id, s.session.id, { cardId: b!.cardId, direction: 'forward', result: 'correct' });
|
||||||
const done = await getNextItem(env.db, s.session.id);
|
const done = await getNextItem(env.db, owner.id, s.session.id);
|
||||||
expect(done).toBeNull();
|
expect(done).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reinserts an incorrect card later in the same session', async () => {
|
it('reinserts an incorrect card later in the same session', async () => {
|
||||||
const l = await seedLesson('L', 4);
|
const l = await seedLesson('L', 4);
|
||||||
const s = await startSession(env.db, { lessonId: l.id, shuffle: false });
|
const s = await startSession(env.db, owner.id, { lessonId: l.id, shuffle: false });
|
||||||
const first = await getNextItem(env.db, s.session.id);
|
const first = await getNextItem(env.db, owner.id, s.session.id);
|
||||||
await recordAttempt(env.db, s.session.id, { cardId: first!.cardId, direction: 'forward', result: 'incorrect' });
|
await recordAttempt(env.db, owner.id, s.session.id, { cardId: first!.cardId, direction: 'forward', result: 'incorrect' });
|
||||||
const seen: number[] = [first!.cardId];
|
const seen: number[] = [first!.cardId];
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
const nx = await getNextItem(env.db, s.session.id);
|
const nx = await getNextItem(env.db, owner.id, s.session.id);
|
||||||
if (!nx) break;
|
if (!nx) break;
|
||||||
seen.push(nx.cardId);
|
seen.push(nx.cardId);
|
||||||
await recordAttempt(env.db, s.session.id, { cardId: nx.cardId, direction: 'forward', result: 'correct' });
|
await recordAttempt(env.db, owner.id, s.session.id, { cardId: nx.cardId, direction: 'forward', result: 'correct' });
|
||||||
}
|
}
|
||||||
expect(seen.filter((c) => c === first!.cardId).length).toBeGreaterThanOrEqual(2);
|
expect(seen.filter((c) => c === first!.cardId).length).toBeGreaterThanOrEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('respects maxCards limit', async () => {
|
it('respects maxCards limit', async () => {
|
||||||
const l = await seedLesson('L', 10);
|
const l = await seedLesson('L', 10);
|
||||||
const s = await startSession(env.db, { lessonId: l.id, shuffle: false, maxCards: 3 });
|
const s = await startSession(env.db, owner.id, { lessonId: l.id, shuffle: false, maxCards: 3 });
|
||||||
expect(s.queue).toHaveLength(3);
|
expect(s.queue).toHaveLength(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('tracks counters on session and ends with duration', async () => {
|
it('tracks counters on session and ends with duration', async () => {
|
||||||
const l = await seedLesson('L', 2);
|
const l = await seedLesson('L', 2);
|
||||||
const s = await startSession(env.db, { lessonId: l.id, shuffle: false });
|
const s = await startSession(env.db, owner.id, { lessonId: l.id, shuffle: false });
|
||||||
const a = await getNextItem(env.db, s.session.id);
|
const a = await getNextItem(env.db, owner.id, s.session.id);
|
||||||
await recordAttempt(env.db, s.session.id, { cardId: a!.cardId, direction: 'forward', result: 'correct' });
|
await recordAttempt(env.db, owner.id, s.session.id, { cardId: a!.cardId, direction: 'forward', result: 'correct' });
|
||||||
const b = await getNextItem(env.db, s.session.id);
|
const b = await getNextItem(env.db, owner.id, s.session.id);
|
||||||
await recordAttempt(env.db, s.session.id, { cardId: b!.cardId, direction: 'forward', result: 'incorrect' });
|
await recordAttempt(env.db, owner.id, s.session.id, { cardId: b!.cardId, direction: 'forward', result: 'incorrect' });
|
||||||
const ended = await endSession(env.db, s.session.id);
|
const ended = await endSession(env.db, owner.id, s.session.id);
|
||||||
expect(ended.cardsCorrect).toBe(1);
|
expect(ended.cardsCorrect).toBe(1);
|
||||||
expect(ended.cardsIncorrect).toBe(1);
|
expect(ended.cardsIncorrect).toBe(1);
|
||||||
expect(ended.cardsShown).toBe(2);
|
expect(ended.cardsShown).toBe(2);
|
||||||
@@ -76,11 +80,11 @@ describe('session engine', () => {
|
|||||||
|
|
||||||
it('returns active session if one exists', async () => {
|
it('returns active session if one exists', async () => {
|
||||||
const l = await seedLesson('L', 1);
|
const l = await seedLesson('L', 1);
|
||||||
const s = await startSession(env.db, { lessonId: l.id });
|
const s = await startSession(env.db, owner.id, { lessonId: l.id });
|
||||||
const active = await getActiveSession(env.db);
|
const active = await getActiveSession(env.db, owner.id);
|
||||||
expect(active?.id).toBe(s.session.id);
|
expect(active?.id).toBe(s.session.id);
|
||||||
await endSession(env.db, s.session.id);
|
await endSession(env.db, owner.id, s.session.id);
|
||||||
const after = await getActiveSession(env.db);
|
const after = await getActiveSession(env.db, owner.id);
|
||||||
expect(after).toBeNull();
|
expect(after).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
} from '@flashcard/shared';
|
} from '@flashcard/shared';
|
||||||
import { applyResult } from './leitner.js';
|
import { applyResult } from './leitner.js';
|
||||||
import { getDescendantLessonIds } from './lessons.js';
|
import { getDescendantLessonIds } from './lessons.js';
|
||||||
|
import { canReadLesson } from './permissions.js';
|
||||||
|
|
||||||
const REINSERT_OFFSET = 3;
|
const REINSERT_OFFSET = 3;
|
||||||
|
|
||||||
@@ -25,17 +26,20 @@ export interface StartedSession {
|
|||||||
queue: QueueItem[];
|
queue: QueueItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startSession(db: Db, input: SessionStartInput): Promise<StartedSession> {
|
export async function startSession(
|
||||||
|
db: Db, userId: number, input: SessionStartInput
|
||||||
|
): Promise<StartedSession> {
|
||||||
const lesson = db.select().from(lessons).where(eq(lessons.id, input.lessonId)).get();
|
const lesson = db.select().from(lessons).where(eq(lessons.id, input.lessonId)).get();
|
||||||
if (!lesson) throw ApiError.notFound('Lesson');
|
if (!lesson) throw ApiError.notFound('Lesson');
|
||||||
|
if (!(await canReadLesson(db, userId, input.lessonId))) {
|
||||||
|
throw new ApiError(403, 'FORBIDDEN_LESSON', 'Cannot start a session for this lesson');
|
||||||
|
}
|
||||||
|
|
||||||
const lessonIds = await getDescendantLessonIds(db, input.lessonId);
|
const lessonIds = await getDescendantLessonIds(db, input.lessonId);
|
||||||
const lessonRows = db.select().from(lessons).where(inArray(lessons.id, lessonIds)).all();
|
const lessonRows = db.select().from(lessons).where(inArray(lessons.id, lessonIds)).all();
|
||||||
const bidirById = new Map(lessonRows.map((l) => [l.id, l.bidirectional]));
|
const bidirById = new Map(lessonRows.map((l) => [l.id, l.bidirectional]));
|
||||||
|
|
||||||
const allCards = lessonIds.length === 0
|
const allCards = db.select().from(cards).where(inArray(cards.lessonId, lessonIds)).all();
|
||||||
? []
|
|
||||||
: db.select().from(cards).where(inArray(cards.lessonId, lessonIds)).all();
|
|
||||||
const direction = input.direction ?? 'forward';
|
const direction = input.direction ?? 'forward';
|
||||||
|
|
||||||
const candidates: QueueItem[] = [];
|
const candidates: QueueItem[] = [];
|
||||||
@@ -49,19 +53,29 @@ export async function startSession(db: Db, input: SessionStartInput): Promise<St
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure per-user progress rows for each candidate
|
||||||
for (const item of candidates) {
|
for (const item of candidates) {
|
||||||
const existing = db.select().from(cardProgress)
|
const existing = db.select().from(cardProgress).where(and(
|
||||||
.where(and(eq(cardProgress.cardId, item.cardId), eq(cardProgress.direction, item.direction)))
|
eq(cardProgress.cardId, item.cardId),
|
||||||
.get();
|
eq(cardProgress.direction, item.direction),
|
||||||
|
eq(cardProgress.userId, userId),
|
||||||
|
)).get();
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
db.insert(cardProgress).values({
|
db.insert(cardProgress).values({
|
||||||
cardId: item.cardId, direction: item.direction, box: 1, nextDueAt: 0,
|
cardId: item.cardId, direction: item.direction,
|
||||||
|
userId, box: 1, nextDueAt: 0,
|
||||||
}).run();
|
}).run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const progressRows = allCards.length === 0
|
const progressRows = allCards.length === 0
|
||||||
? []
|
? []
|
||||||
: db.select().from(cardProgress).where(inArray(cardProgress.cardId, allCards.map((c) => c.id))).all();
|
: db.select().from(cardProgress)
|
||||||
|
.where(and(
|
||||||
|
inArray(cardProgress.cardId, allCards.map((c) => c.id)),
|
||||||
|
eq(cardProgress.userId, userId),
|
||||||
|
))
|
||||||
|
.all();
|
||||||
const progByKey = new Map<string, typeof progressRows[number]>();
|
const progByKey = new Map<string, typeof progressRows[number]>();
|
||||||
for (const p of progressRows) progByKey.set(`${p.cardId}:${p.direction}`, p);
|
for (const p of progressRows) progByKey.set(`${p.cardId}:${p.direction}`, p);
|
||||||
|
|
||||||
@@ -88,10 +102,11 @@ export async function startSession(db: Db, input: SessionStartInput): Promise<St
|
|||||||
|
|
||||||
let queue: QueueItem[] = [...due, ...future];
|
let queue: QueueItem[] = [...due, ...future];
|
||||||
const max = input.maxCards ?? null;
|
const max = input.maxCards ?? null;
|
||||||
if (max !== null && max !== undefined) queue = queue.slice(0, max);
|
if (max !== null) queue = queue.slice(0, max);
|
||||||
|
|
||||||
const [row] = db.insert(sessions).values({
|
const [row] = db.insert(sessions).values({
|
||||||
lessonId: input.lessonId,
|
lessonId: input.lessonId,
|
||||||
|
userId,
|
||||||
queueSnapshot: JSON.stringify({ remaining: queue, index: 0 }),
|
queueSnapshot: JSON.stringify({ remaining: queue, index: 0 }),
|
||||||
}).returning().all();
|
}).returning().all();
|
||||||
|
|
||||||
@@ -118,22 +133,25 @@ function readQueue(snapshot: string | null | undefined): QueueState {
|
|||||||
return JSON.parse(snapshot) as QueueState;
|
return JSON.parse(snapshot) as QueueState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNextItem(db: Db, sessionId: number): Promise<QueueItem | null> {
|
function assertSessionOwnership(sess: { userId: number | null } | undefined, userId: number) {
|
||||||
|
if (!sess) throw ApiError.notFound('Session');
|
||||||
|
if (sess.userId !== userId) throw new ApiError(403, 'FORBIDDEN_LESSON', 'Not your session');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNextItem(db: Db, userId: number, sessionId: number): Promise<QueueItem | null> {
|
||||||
const row = db.select().from(sessions).where(eq(sessions.id, sessionId)).get();
|
const row = db.select().from(sessions).where(eq(sessions.id, sessionId)).get();
|
||||||
if (!row) throw ApiError.notFound('Session');
|
assertSessionOwnership(row, userId);
|
||||||
if (row.status !== 'active') return null;
|
if (row!.status !== 'active') return null;
|
||||||
const state = readQueue(row.queueSnapshot);
|
const state = readQueue(row!.queueSnapshot);
|
||||||
return state.remaining[state.index] ?? null;
|
return state.remaining[state.index] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function recordAttempt(
|
export async function recordAttempt(
|
||||||
db: Db,
|
db: Db, userId: number, sessionId: number, input: AttemptCreateInput
|
||||||
sessionId: number,
|
|
||||||
input: AttemptCreateInput
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const sess = db.select().from(sessions).where(eq(sessions.id, sessionId)).get();
|
const sess = db.select().from(sessions).where(eq(sessions.id, sessionId)).get();
|
||||||
if (!sess) throw ApiError.notFound('Session');
|
assertSessionOwnership(sess, userId);
|
||||||
if (sess.status !== 'active') throw ApiError.validation('Session is not active');
|
if (sess!.status !== 'active') throw ApiError.validation('Session is not active');
|
||||||
|
|
||||||
const now = nowSec();
|
const now = nowSec();
|
||||||
db.insert(attempts).values({
|
db.insert(attempts).values({
|
||||||
@@ -144,9 +162,11 @@ export async function recordAttempt(
|
|||||||
timeToAnswerMs: input.timeToAnswerMs ?? null,
|
timeToAnswerMs: input.timeToAnswerMs ?? null,
|
||||||
}).run();
|
}).run();
|
||||||
|
|
||||||
const prog = db.select().from(cardProgress)
|
const prog = db.select().from(cardProgress).where(and(
|
||||||
.where(and(eq(cardProgress.cardId, input.cardId), eq(cardProgress.direction, input.direction)))
|
eq(cardProgress.cardId, input.cardId),
|
||||||
.get();
|
eq(cardProgress.direction, input.direction),
|
||||||
|
eq(cardProgress.userId, userId),
|
||||||
|
)).get();
|
||||||
if (prog) {
|
if (prog) {
|
||||||
const delta = applyResult(
|
const delta = applyResult(
|
||||||
{ box: prog.box, correctCount: prog.correctCount, incorrectCount: prog.incorrectCount },
|
{ box: prog.box, correctCount: prog.correctCount, incorrectCount: prog.incorrectCount },
|
||||||
@@ -159,10 +179,14 @@ export async function recordAttempt(
|
|||||||
incorrectCount: delta.incorrectCount,
|
incorrectCount: delta.incorrectCount,
|
||||||
nextDueAt: delta.nextDueAt,
|
nextDueAt: delta.nextDueAt,
|
||||||
lastShownAt: delta.lastShownAt,
|
lastShownAt: delta.lastShownAt,
|
||||||
}).where(and(eq(cardProgress.cardId, input.cardId), eq(cardProgress.direction, input.direction))).run();
|
}).where(and(
|
||||||
|
eq(cardProgress.cardId, input.cardId),
|
||||||
|
eq(cardProgress.direction, input.direction),
|
||||||
|
eq(cardProgress.userId, userId),
|
||||||
|
)).run();
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = readQueue(sess.queueSnapshot);
|
const state = readQueue(sess!.queueSnapshot);
|
||||||
state.index += 1;
|
state.index += 1;
|
||||||
if (input.result === 'incorrect') {
|
if (input.result === 'incorrect') {
|
||||||
const insertAt = Math.min(state.remaining.length, state.index + REINSERT_OFFSET);
|
const insertAt = Math.min(state.remaining.length, state.index + REINSERT_OFFSET);
|
||||||
@@ -171,45 +195,47 @@ export async function recordAttempt(
|
|||||||
|
|
||||||
db.update(sessions).set({
|
db.update(sessions).set({
|
||||||
queueSnapshot: JSON.stringify(state),
|
queueSnapshot: JSON.stringify(state),
|
||||||
cardsShown: sess.cardsShown + 1,
|
cardsShown: sess!.cardsShown + 1,
|
||||||
cardsCorrect: sess.cardsCorrect + (input.result === 'correct' ? 1 : 0),
|
cardsCorrect: sess!.cardsCorrect + (input.result === 'correct' ? 1 : 0),
|
||||||
cardsIncorrect: sess.cardsIncorrect + (input.result === 'incorrect' ? 1 : 0),
|
cardsIncorrect: sess!.cardsIncorrect + (input.result === 'incorrect' ? 1 : 0),
|
||||||
}).where(eq(sessions.id, sessionId)).run();
|
}).where(eq(sessions.id, sessionId)).run();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function endSession(db: Db, sessionId: number): Promise<SessionRow> {
|
export async function endSession(db: Db, userId: number, sessionId: number): Promise<SessionRow> {
|
||||||
const sess = db.select().from(sessions).where(eq(sessions.id, sessionId)).get();
|
const sess = db.select().from(sessions).where(eq(sessions.id, sessionId)).get();
|
||||||
if (!sess) throw ApiError.notFound('Session');
|
assertSessionOwnership(sess, userId);
|
||||||
const endedAt = nowSec();
|
const endedAt = nowSec();
|
||||||
const duration = endedAt - sess.startedAt;
|
const duration = endedAt - sess!.startedAt;
|
||||||
const [row] = db.update(sessions).set({
|
const [row] = db.update(sessions).set({
|
||||||
status: 'completed',
|
status: 'completed', endedAt, durationSeconds: duration, queueSnapshot: null,
|
||||||
endedAt,
|
|
||||||
durationSeconds: duration,
|
|
||||||
queueSnapshot: null,
|
|
||||||
}).where(eq(sessions.id, sessionId)).returning().all();
|
}).where(eq(sessions.id, sessionId)).returning().all();
|
||||||
return rowToSession(row!);
|
return rowToSession(row!);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function abandonSession(db: Db, sessionId: number): Promise<SessionRow> {
|
export async function abandonSession(db: Db, userId: number, sessionId: number): Promise<SessionRow> {
|
||||||
const sess = db.select().from(sessions).where(eq(sessions.id, sessionId)).get();
|
const sess = db.select().from(sessions).where(eq(sessions.id, sessionId)).get();
|
||||||
if (!sess) throw ApiError.notFound('Session');
|
assertSessionOwnership(sess, userId);
|
||||||
const endedAt = nowSec();
|
const endedAt = nowSec();
|
||||||
const duration = endedAt - sess.startedAt;
|
const duration = endedAt - sess!.startedAt;
|
||||||
const [row] = db.update(sessions).set({
|
const [row] = db.update(sessions).set({
|
||||||
status: 'abandoned', endedAt, durationSeconds: duration,
|
status: 'abandoned', endedAt, durationSeconds: duration,
|
||||||
}).where(eq(sessions.id, sessionId)).returning().all();
|
}).where(eq(sessions.id, sessionId)).returning().all();
|
||||||
return rowToSession(row!);
|
return rowToSession(row!);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getActiveSession(db: Db): Promise<SessionRow | null> {
|
export async function getActiveSession(db: Db, userId: number): Promise<SessionRow | null> {
|
||||||
const row = db.select().from(sessions).where(eq(sessions.status, 'active')).orderBy(sql`${sessions.startedAt} DESC`).get();
|
const row = db.select().from(sessions)
|
||||||
|
.where(and(eq(sessions.status, 'active'), eq(sessions.userId, userId)))
|
||||||
|
.orderBy(sql`${sessions.startedAt} DESC`).get();
|
||||||
return row ? rowToSession(row) : null;
|
return row ? rowToSession(row) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSessionState(db: Db, sessionId: number): Promise<{ session: SessionRow; queue: QueueItem[]; index: number } | null> {
|
export async function getSessionState(
|
||||||
|
db: Db, userId: number, sessionId: number
|
||||||
|
): Promise<{ session: SessionRow; queue: QueueItem[]; index: number } | null> {
|
||||||
const row = db.select().from(sessions).where(eq(sessions.id, sessionId)).get();
|
const row = db.select().from(sessions).where(eq(sessions.id, sessionId)).get();
|
||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
|
if (row.userId !== userId) return null;
|
||||||
const state = readQueue(row.queueSnapshot);
|
const state = readQueue(row.queueSnapshot);
|
||||||
return { session: rowToSession(row), queue: state.remaining, index: state.index };
|
return { session: rowToSession(row), queue: state.remaining, index: state.index };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user