Skip to content

Commit

Permalink
Merge pull request #176 from bradleyayers/quiz-fixes
Browse files Browse the repository at this point in the history
Fix quiz bugs
  • Loading branch information
bradleyayers authored Jun 25, 2024
2 parents 96c4110 + 2e68d9e commit dcd1b05
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 15 deletions.
26 changes: 18 additions & 8 deletions projects/frontend/src/app/learn/quiz.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { RectButton } from "@/components/RectButton";
import { useQueryOnce } from "@/components/ReplicacheContext";
import { RootView } from "@/components/RootView";
import { generateQuestionForSkill } from "@/data/generator";
import { unmarshalSkillStateJson } from "@/data/marshal";
import { IndexName, indexScan } from "@/data/marshal";
import { formatDuration } from "date-fns/formatDuration";
import { interval } from "date-fns/interval";
import { intervalToDuration } from "date-fns/intervalToDuration";
import { router } from "expo-router";
import { StatusBar as ExpoStatusBar } from "expo-status-bar";
import shuffle from "lodash/shuffle";
import take from "lodash/take";
import { StyleSheet, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";

Expand All @@ -17,17 +19,25 @@ export default function QuizPage() {

const questions = useQueryOnce(async (tx) => {
const now = new Date();
return (await tx.scan({ prefix: `s/`, limit: 10 }).entries().toArray())
.map(unmarshalSkillStateJson)
.filter(([, state]) => state.due <= now)
.map(([skill]) => generateQuestionForSkill(skill));

// Look ahead at the next 50 skills, shuffle them and take 10. This way
// you don't end up with the same set over and over again (which happens a
// lot in development).
return take(
shuffle(
(await indexScan(tx, IndexName.SkillStateByDue, 50)).filter(
([, { due }]) => due <= now,
),
),
10,
).map(([skill]) => generateQuestionForSkill(skill));
});

const nextSkillState = useQueryOnce(
async (tx) =>
(await tx.scan({ prefix: `s/`, limit: 1 }).entries().toArray())
.map(unmarshalSkillStateJson)
.map(([, skillState]) => skillState)[0],
(await indexScan(tx, IndexName.SkillStateByDue, 1)).map(
([, skillState]) => skillState,
)[0],
);

return (
Expand Down
2 changes: 1 addition & 1 deletion projects/frontend/src/components/QuizDeck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const QuizDeck = ({
const next = new Map(prev);
const prevState = prev.get(currentQuestion);

const attempts = prevState?.attempts ?? 1;
const attempts = (prevState?.attempts ?? 0) + 1;
next.set(currentQuestion, {
type: success
? QuestionStateType.Correct
Expand Down
2 changes: 2 additions & 0 deletions projects/frontend/src/components/ReplicacheContext.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { indexes } from "@/data/marshal";
import { mutators } from "@/data/mutators";
import { replicacheLicenseKey } from "@/env";
import { invariant } from "@/util/invariant";
Expand Down Expand Up @@ -56,6 +57,7 @@ export function ReplicacheProvider({ children }: React.PropsWithChildren) {
// },
experimentalCreateKVStore,
mutators,
indexes,
}),
[],
);
Expand Down
57 changes: 51 additions & 6 deletions projects/frontend/src/data/marshal.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { iterTake } from "@/util/collections";
import { invariant } from "@/util/invariant";
import { ReadonlyJSONValue } from "replicache";
import {
IndexDefinitions,
ReadonlyJSONValue,
ReadTransaction,
} from "replicache";
import z from "zod";
import {
Skill,
Expand Down Expand Up @@ -117,10 +122,8 @@ const MarshaledSkillStateValue = z.object({
});
export type MarshaledSkillStateValue = z.infer<typeof MarshaledSkillStateValue>;

// export type MarshaledSkillState = [
// key: MarshaledSkillStateKey,
// value: MarshaledSkillStateValue,
// ];
export type MarshaledSkillStateValueJsonPath =
`/${keyof MarshaledSkillStateValue}`;

export const marshalSkillStateValue = (
x: SkillStateValue,
Expand Down Expand Up @@ -177,9 +180,11 @@ export const marshalSkillId = (x: Skill) =>

export const marshalSkillStateKey = (x: Skill | MarshaledSkillId) => {
const id = typeof x === `string` ? x : marshalSkillId(x);
return `s/${id}` as MarshaledSkillStateKey;
return `${skillStatePrefix}${id}` as MarshaledSkillStateKey;
};

export const skillStatePrefix = `s/`;

export const parseSkillStateKey = (x: string): Skill => {
const result = x.match(/^s\/(.+)$/);
invariant(result !== null);
Expand All @@ -204,3 +209,43 @@ export const parseSkillId = (x: string): Skill => {
};

export type Timestamp = number;

export enum IndexName {
Null = `Null`,
SkillStateByDue = `SkillStateByDue`,
}

export const indexes = {
[IndexName.Null]: {
allowEmpty: true,
prefix: `///`,
jsonPointer: `/d` satisfies MarshaledSkillStateValueJsonPath,
},
[IndexName.SkillStateByDue]: {
allowEmpty: false,
prefix: skillStatePrefix,
jsonPointer: `/d` satisfies MarshaledSkillStateValueJsonPath,
},
} satisfies IndexDefinitions;

export const indexUnmarshalers = {
[IndexName.Null]: () => null,
[IndexName.SkillStateByDue]: unmarshalSkillStateJson,
};

export async function indexScan<
I extends IndexName,
Unmarshaled = ReturnType<(typeof indexUnmarshalers)[I]>,
>(tx: ReadTransaction, indexName: I, limit?: number): Promise<Unmarshaled[]> {
// Work around https://github.com/rocicorp/replicache/issues/1039
const iter = tx.scan({ indexName }).entries();
const results = limit ? await iterTake(iter, limit) : await iter.toArray();
const unmarshal = indexUnmarshalers[indexName];

return (
results
// Strip off the secondary key, put it back in the normal "no index" mode
// of [key, value].
.map(([[, key], value]) => unmarshal([key, value]) as Unmarshaled)
);
}
13 changes: 13 additions & 0 deletions projects/frontend/src/util/collections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export async function iterTake<T>(
iter: AsyncIterableIterator<T>,
limit: number,
): Promise<T[]> {
const results: T[] = [];
for await (const x of iter) {
results.push(x);
if (results.length === limit) {
break;
}
}
return results;
}

0 comments on commit dcd1b05

Please sign in to comment.