From d12e2ac5b26f4cb244aa6420433f535b8f3e983e Mon Sep 17 00:00:00 2001 From: SillyCoon Date: Thu, 2 Jul 2026 08:23:05 +0900 Subject: [PATCH 1/9] use live collections to fetch gp data --- src/live.config.ts | 107 +++++++++++++++++++ src/pages/grammar/[id]/index.astro | 21 +++- src/pages/grammar/[id]/practice.astro | 11 +- src/pages/grammar/index.astro | 14 ++- src/pages/grammar/practice/result.astro | 8 +- src/pages/sr/review/[sessionId]/result.astro | 11 +- 6 files changed, 156 insertions(+), 16 deletions(-) create mode 100644 src/live.config.ts diff --git a/src/live.config.ts b/src/live.config.ts new file mode 100644 index 0000000..ea76cf8 --- /dev/null +++ b/src/live.config.ts @@ -0,0 +1,107 @@ +// Define live collections for accessing real-time data +import { defineLiveCollection } from "astro:content"; +import type { LiveLoader } from "astro/loaders"; +import { + type Context, + type GrammarPoint, + fetchAllGrammarPoints, + fetchGrammarPoint, + fetchGrammarPoints, +} from "grammar-sdk"; + +function grammarPointsLoader(): LiveLoader< + GrammarPoint, + { id: string; context: Context }, + { ids: string[]; context: Context } | { context: Context } +> { + return { + name: "grammarPoints", + loadCollection: async ({ filter }) => { + try { + if (filter && "ids" in filter) { + const entries = await fetchGrammarPoints(filter.ids, filter.context); + return { + entries: entries.map((entry) => ({ + id: entry.id, + data: entry, + cacheHint: { + tags: [`grammar-point-${entry.id}`], + }, + })), + }; + } + if (!filter) { + const entries = await fetchAllGrammarPoints({ + user: { role: "guest" }, + }); + return { + entries: entries.map((entry) => ({ + id: entry.id, + data: entry, + cacheHint: { + tags: [`grammar-point-${entry.id}`], + }, + })), + cacheHint: { + tags: ["grammar-points"], + }, + }; + } + const entries = await fetchAllGrammarPoints(filter.context); + return { + entries: entries.map((entry) => ({ + id: entry.id, + data: entry, + cacheHint: { + tags: [`grammar-point-${entry.id}`], + }, + })), + cacheHint: { + tags: ["grammar-points"], + }, + }; + } catch (error) { + console.error("Error loading grammar points:", error); + return { + error: + error instanceof Error + ? error + : new Error("Failed to load grammar points", { cause: error }), + }; + } + }, + loadEntry: async ({ filter: { id, context } }) => { + try { + const entry = await fetchGrammarPoint(id, context); + if (!entry) { + return { + error: new Error(`Grammar point with id ${id} not found`), + }; + } + return { + id: entry.id, + data: entry, + cacheHint: { + tags: [`grammar-point-${entry.id}`], + }, + }; + } catch (error) { + console.error(`Error loading grammar point with id ${id}:`, error); + return { + error: + error instanceof Error + ? error + : new Error(`Failed to load grammar point with id ${id}`, { + cause: error, + }), + }; + } + }, + }; +} + +const grammarPoints = defineLiveCollection({ + loader: grammarPointsLoader(), +}); + +export const collections = { grammarPoints }; diff --git a/src/pages/grammar/[id]/index.astro b/src/pages/grammar/[id]/index.astro index 2df5449..552fc8e 100644 --- a/src/pages/grammar/[id]/index.astro +++ b/src/pages/grammar/[id]/index.astro @@ -2,24 +2,37 @@ import { GET as fetchGpInReview } from "@api/sr/review/list"; import { GrammarPoint } from "@components/solid/grammar-point/GrammarPoint"; import { ReviewToggleButton } from "@components/solid/grammar-point/ReviewToggleButton"; -import { fetchGrammarPoint } from "grammar-sdk"; import { Button } from "packages/ui/button"; import { Anchor } from "ui/anchor"; import Layout from "../../../layouts/Layout.astro"; import { contextFromAstro } from "~/libs/context"; +import { getLiveEntry } from "astro:content"; const { id } = Astro.params; const backTo = Astro.url.searchParams.get("backTo") ?? undefined; -const grammarPoint = id - ? await fetchGrammarPoint(id, contextFromAstro(Astro)) - : undefined; +if (!id || isNaN(Number(id))) { + return Astro.redirect("/grammar"); +} + +const { entry: grammarPointEntry, error: grammarPointError } = + await getLiveEntry("grammarPoints", { + id, + context: contextFromAstro(Astro), + }); + +if (!grammarPointEntry || grammarPointError || !grammarPointEntry) { + return Astro.redirect("/grammar"); +} + const inReview = id ? (( (await (await fetchGpInReview(Astro)).json()) ?? ([] as string[]) )?.includes?.(id) ?? false) : false; const user = Astro.locals.user; + +const grammarPoint = grammarPointEntry.data; --- diff --git a/src/pages/grammar/[id]/practice.astro b/src/pages/grammar/[id]/practice.astro index 71facbb..69c0b81 100644 --- a/src/pages/grammar/[id]/practice.astro +++ b/src/pages/grammar/[id]/practice.astro @@ -1,14 +1,19 @@ --- -import { fetchGrammarPoints } from "grammar-sdk"; import { simpleShuffle } from "utils/array"; import { Exercises } from "../../../components/solid/Exercise/Exercises"; import Layout from "../../../layouts/Layout.astro"; import { contextFromAstro } from "~/libs/context"; +import { getLiveCollection } from "astro:content"; const ids = Astro.params.id?.split("-") ?? []; -const grammarPoints = await fetchGrammarPoints(ids, contextFromAstro(Astro)); -const exercises = simpleShuffle(grammarPoints.flatMap((g) => g.exercises)); +const { entries: grammarPoints } = await getLiveCollection("grammarPoints", { + ids, + context: contextFromAstro(Astro), +}); +const exercises = simpleShuffle( + (grammarPoints ?? []).flatMap((g) => g.data.exercises), +); --- diff --git a/src/pages/grammar/index.astro b/src/pages/grammar/index.astro index 9642ab6..b641b6f 100644 --- a/src/pages/grammar/index.astro +++ b/src/pages/grammar/index.astro @@ -1,16 +1,24 @@ --- import { GET as fetchGpInReview } from "@api/sr/review/list"; -import { fetchAllGrammarPoints } from "grammar-sdk"; import { Grammar } from "../../components/solid/grammar/Grammar"; import Layout from "../../layouts/Layout.astro"; import { contextFromAstro } from "~/libs/context"; +import { getLiveCollection } from "astro:content"; + +const grammar = await getLiveCollection("grammarPoints", { + context: contextFromAstro(Astro), +}); -const grammar = await fetchAllGrammarPoints(contextFromAstro(Astro)); const inReview = (await (await fetchGpInReview(Astro)).json()) as string[]; const maybeMode = Astro.url.searchParams.get("mode"); const mode = maybeMode === "cram" ? "cram" : "nav"; --- - + entry.data) ?? []} + inReview={inReview} + mode={mode} + /> diff --git a/src/pages/grammar/practice/result.astro b/src/pages/grammar/practice/result.astro index 9155ee9..b462170 100644 --- a/src/pages/grammar/practice/result.astro +++ b/src/pages/grammar/practice/result.astro @@ -1,16 +1,18 @@ --- import { SessionResult } from "@components/solid/Exercise/SessionResult"; import Layout from "@layouts/Layout.astro"; -import { fetchAllGrammarPoints } from "grammar-sdk"; +import { getLiveCollection } from "astro:content"; import { contextFromAstro } from "~/libs/context"; -const grammar = await fetchAllGrammarPoints(contextFromAstro(Astro)); +const grammar = await getLiveCollection("grammarPoints", { + context: contextFromAstro(Astro), +}); --- entry.data) ?? []} /> diff --git a/src/pages/sr/review/[sessionId]/result.astro b/src/pages/sr/review/[sessionId]/result.astro index 59e3b8a..397e4d1 100644 --- a/src/pages/sr/review/[sessionId]/result.astro +++ b/src/pages/sr/review/[sessionId]/result.astro @@ -2,7 +2,7 @@ import { SessionResult } from "@components/solid/Exercise/SessionResult"; import Layout from "@layouts/Layout.astro"; import { actions } from "astro:actions"; -import { fetchAllGrammarPoints } from "grammar-sdk"; +import { getLiveCollection } from "astro:content"; import { contextFromAstro } from "~/libs/context"; const sessionResult = Astro.params.sessionId @@ -10,13 +10,18 @@ const sessionResult = Astro.params.sessionId sessionId: Astro.params.sessionId, }) : undefined; -const grammar = await fetchAllGrammarPoints(contextFromAstro(Astro)); +const grammar = await getLiveCollection("grammarPoints", { + context: contextFromAstro(Astro), +}); --- { sessionResult?.data ? ( - + entry.data) ?? []} + /> ) : (
No session result found, probably it hasn't been calculated yet. Please From a0576ee8661582d906d4000d00701bff7f9fcf36 Mon Sep 17 00:00:00 2001 From: SillyCoon Date: Thu, 2 Jul 2026 18:40:59 +0900 Subject: [PATCH 2/9] add error page --- src/features/grammar-point/loader/errors.ts | 16 ++++++++++++++++ src/middleware.ts | 2 ++ 2 files changed, 18 insertions(+) create mode 100644 src/features/grammar-point/loader/errors.ts diff --git a/src/features/grammar-point/loader/errors.ts b/src/features/grammar-point/loader/errors.ts new file mode 100644 index 0000000..904a229 --- /dev/null +++ b/src/features/grammar-point/loader/errors.ts @@ -0,0 +1,16 @@ +export class GrammarPointsLoaderError extends Error { + constructor( + message: string, + public code?: number, + ) { + super(message); + this.name = "GrammarPointsLoaderError"; + } +} +export class GrammarPointNotFoundError extends GrammarPointsLoaderError { + constructor(message: string) { + super(message); + this.name = "GrammarPointNotFoundError"; + this.code = 404; + } +} diff --git a/src/middleware.ts b/src/middleware.ts index db45630..d8939e7 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -4,6 +4,7 @@ import { isUserAdmin } from "libs/auth/admin"; const PATHS_TO_IGNORE = [ "signin", + "signup", "register", "auth", "login", @@ -12,6 +13,7 @@ const PATHS_TO_IGNORE = [ "help", "privacy-policy", "sitemap.xml", + "error", ]; export const onRequest = defineMiddleware( From aba783027b3480cc225b17a233dc2eeceac95c8c Mon Sep 17 00:00:00 2001 From: SillyCoon Date: Thu, 2 Jul 2026 19:00:25 +0900 Subject: [PATCH 3/9] improve grammar live collection --- src/features/grammar-point/loader/index.ts | 75 +++++++++++++++ src/live.config.ts | 101 +-------------------- 2 files changed, 76 insertions(+), 100 deletions(-) create mode 100644 src/features/grammar-point/loader/index.ts diff --git a/src/features/grammar-point/loader/index.ts b/src/features/grammar-point/loader/index.ts new file mode 100644 index 0000000..d968cb2 --- /dev/null +++ b/src/features/grammar-point/loader/index.ts @@ -0,0 +1,75 @@ +import type { LiveLoader } from "astro/loaders"; +import { + type Context, + type GrammarPoint, + fetchAllGrammarPoints, + fetchGrammarPoint, + fetchGrammarPoints, +} from "grammar-sdk"; +import logger from "libs/logger"; +import { GrammarPointNotFoundError, GrammarPointsLoaderError } from "./errors"; + +const grammarPointsToEntries = (grammarPoints: GrammarPoint[]) => ({ + entries: grammarPoints.map(grammarPointToEntry), +}); + +const grammarPointToEntry = (grammarPoint: GrammarPoint) => ({ + id: grammarPoint.id, + data: grammarPoint, + cacheHint: { + tags: [`grammar-point-${grammarPoint.id}`], + }, +}); + +export function grammarPointsLoader(): LiveLoader< + GrammarPoint, + { id: string; context: Context }, + { ids: string[]; context: Context } | { context: Context } +> { + return { + name: "grammarPoints", + loadCollection: async ({ filter }) => { + try { + if (filter && "ids" in filter) { + return grammarPointsToEntries( + await fetchGrammarPoints(filter.ids, filter.context), + ); + } + + const context = filter?.context ?? { user: { role: "guest" } }; + + return { + ...grammarPointsToEntries(await fetchAllGrammarPoints(context)), + cacheHint: { + tags: [`grammar-points-${context.user.role}`], + }, + }; + } catch (error) { + const message = "Failed to load grammar points"; + logger.error(error, message); + return { + error: new GrammarPointsLoaderError(message, 500), + }; + } + }, + loadEntry: async ({ filter: { id, context } }) => { + try { + const entry = await fetchGrammarPoint(id, context); + if (!entry) { + return { + error: new GrammarPointNotFoundError( + `Grammar point with id ${id} not found`, + ), + }; + } + return grammarPointToEntry(entry); + } catch (error) { + const message = `Failed to load grammar point with id ${id}`; + logger.error(error, message); + return { + error: new GrammarPointsLoaderError(message, 500), + }; + } + }, + }; +} diff --git a/src/live.config.ts b/src/live.config.ts index ea76cf8..8490db4 100644 --- a/src/live.config.ts +++ b/src/live.config.ts @@ -1,104 +1,5 @@ -// Define live collections for accessing real-time data import { defineLiveCollection } from "astro:content"; -import type { LiveLoader } from "astro/loaders"; -import { - type Context, - type GrammarPoint, - fetchAllGrammarPoints, - fetchGrammarPoint, - fetchGrammarPoints, -} from "grammar-sdk"; - -function grammarPointsLoader(): LiveLoader< - GrammarPoint, - { id: string; context: Context }, - { ids: string[]; context: Context } | { context: Context } -> { - return { - name: "grammarPoints", - loadCollection: async ({ filter }) => { - try { - if (filter && "ids" in filter) { - const entries = await fetchGrammarPoints(filter.ids, filter.context); - return { - entries: entries.map((entry) => ({ - id: entry.id, - data: entry, - cacheHint: { - tags: [`grammar-point-${entry.id}`], - }, - })), - }; - } - if (!filter) { - const entries = await fetchAllGrammarPoints({ - user: { role: "guest" }, - }); - return { - entries: entries.map((entry) => ({ - id: entry.id, - data: entry, - cacheHint: { - tags: [`grammar-point-${entry.id}`], - }, - })), - cacheHint: { - tags: ["grammar-points"], - }, - }; - } - const entries = await fetchAllGrammarPoints(filter.context); - return { - entries: entries.map((entry) => ({ - id: entry.id, - data: entry, - cacheHint: { - tags: [`grammar-point-${entry.id}`], - }, - })), - cacheHint: { - tags: ["grammar-points"], - }, - }; - } catch (error) { - console.error("Error loading grammar points:", error); - return { - error: - error instanceof Error - ? error - : new Error("Failed to load grammar points", { cause: error }), - }; - } - }, - loadEntry: async ({ filter: { id, context } }) => { - try { - const entry = await fetchGrammarPoint(id, context); - if (!entry) { - return { - error: new Error(`Grammar point with id ${id} not found`), - }; - } - return { - id: entry.id, - data: entry, - cacheHint: { - tags: [`grammar-point-${entry.id}`], - }, - }; - } catch (error) { - console.error(`Error loading grammar point with id ${id}:`, error); - return { - error: - error instanceof Error - ? error - : new Error(`Failed to load grammar point with id ${id}`, { - cause: error, - }), - }; - } - }, - }; -} +import { grammarPointsLoader } from "./features/grammar-point/loader"; const grammarPoints = defineLiveCollection({ loader: grammarPointsLoader(), From 2e470f2cf6b3e8044d19aa45e349d4546386c2f5 Mon Sep 17 00:00:00 2001 From: SillyCoon Date: Thu, 2 Jul 2026 19:00:35 +0900 Subject: [PATCH 4/9] add error page --- src/pages/error.astro | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/pages/error.astro diff --git a/src/pages/error.astro b/src/pages/error.astro new file mode 100644 index 0000000..b80742e --- /dev/null +++ b/src/pages/error.astro @@ -0,0 +1,15 @@ +--- +export const prerender = true; +import Layout from "~/layouts/Layout.astro"; +--- + + +
+
+

+ We're experiencing technical difficulties. Please try again later. +

+ Go back to home +
+
+
From fc650d63217034bbad059ab1667f636bde294ff0 Mon Sep 17 00:00:00 2001 From: SillyCoon Date: Thu, 2 Jul 2026 19:00:43 +0900 Subject: [PATCH 5/9] improve live collection error handling --- src/pages/grammar/[id]/index.astro | 2 +- src/pages/grammar/[id]/practice.astro | 16 ++++++++++++---- src/pages/grammar/index.astro | 8 ++++++-- src/pages/grammar/practice/result.astro | 8 ++++++-- src/pages/sr/review/[sessionId]/result.astro | 8 ++++++-- 5 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/pages/grammar/[id]/index.astro b/src/pages/grammar/[id]/index.astro index 552fc8e..e8be716 100644 --- a/src/pages/grammar/[id]/index.astro +++ b/src/pages/grammar/[id]/index.astro @@ -21,7 +21,7 @@ const { entry: grammarPointEntry, error: grammarPointError } = context: contextFromAstro(Astro), }); -if (!grammarPointEntry || grammarPointError || !grammarPointEntry) { +if (!grammarPointEntry || grammarPointError) { return Astro.redirect("/grammar"); } diff --git a/src/pages/grammar/[id]/practice.astro b/src/pages/grammar/[id]/practice.astro index 69c0b81..605da0b 100644 --- a/src/pages/grammar/[id]/practice.astro +++ b/src/pages/grammar/[id]/practice.astro @@ -7,10 +7,18 @@ import { getLiveCollection } from "astro:content"; const ids = Astro.params.id?.split("-") ?? []; -const { entries: grammarPoints } = await getLiveCollection("grammarPoints", { - ids, - context: contextFromAstro(Astro), -}); +const { entries: grammarPoints, error } = await getLiveCollection( + "grammarPoints", + { + ids, + context: contextFromAstro(Astro), + }, +); + +if (error) { + return Astro.redirect("/error"); +} + const exercises = simpleShuffle( (grammarPoints ?? []).flatMap((g) => g.data.exercises), ); diff --git a/src/pages/grammar/index.astro b/src/pages/grammar/index.astro index b641b6f..34c2ba1 100644 --- a/src/pages/grammar/index.astro +++ b/src/pages/grammar/index.astro @@ -5,10 +5,14 @@ import Layout from "../../layouts/Layout.astro"; import { contextFromAstro } from "~/libs/context"; import { getLiveCollection } from "astro:content"; -const grammar = await getLiveCollection("grammarPoints", { +const { entries, error } = await getLiveCollection("grammarPoints", { context: contextFromAstro(Astro), }); +if (error) { + return Astro.redirect("/error"); +} + const inReview = (await (await fetchGpInReview(Astro)).json()) as string[]; const maybeMode = Astro.url.searchParams.get("mode"); const mode = maybeMode === "cram" ? "cram" : "nav"; @@ -17,7 +21,7 @@ const mode = maybeMode === "cram" ? "cram" : "nav"; entry.data) ?? []} + grammar={entries?.map((entry) => entry.data) ?? []} inReview={inReview} mode={mode} /> diff --git a/src/pages/grammar/practice/result.astro b/src/pages/grammar/practice/result.astro index b462170..44946c3 100644 --- a/src/pages/grammar/practice/result.astro +++ b/src/pages/grammar/practice/result.astro @@ -4,15 +4,19 @@ import Layout from "@layouts/Layout.astro"; import { getLiveCollection } from "astro:content"; import { contextFromAstro } from "~/libs/context"; -const grammar = await getLiveCollection("grammarPoints", { +const { entries: grammar, error } = await getLiveCollection("grammarPoints", { context: contextFromAstro(Astro), }); + +if (error) { + return Astro.redirect("/error"); +} --- entry.data) ?? []} + grammar={grammar?.map((entry) => entry.data) ?? []} /> diff --git a/src/pages/sr/review/[sessionId]/result.astro b/src/pages/sr/review/[sessionId]/result.astro index 397e4d1..f87b3c1 100644 --- a/src/pages/sr/review/[sessionId]/result.astro +++ b/src/pages/sr/review/[sessionId]/result.astro @@ -10,9 +10,13 @@ const sessionResult = Astro.params.sessionId sessionId: Astro.params.sessionId, }) : undefined; -const grammar = await getLiveCollection("grammarPoints", { +const { entries: grammar, error } = await getLiveCollection("grammarPoints", { context: contextFromAstro(Astro), }); + +if (error) { + return Astro.redirect("/error"); +} --- @@ -20,7 +24,7 @@ const grammar = await getLiveCollection("grammarPoints", { sessionResult?.data ? ( entry.data) ?? []} + grammar={grammar?.map((entry) => entry.data) ?? []} /> ) : (
From ec88adc2ee7b2a255e39d46bf88717c2f87c0df3 Mon Sep 17 00:00:00 2001 From: SillyCoon Date: Thu, 2 Jul 2026 19:09:03 +0900 Subject: [PATCH 6/9] remove error prerender --- src/pages/error.astro | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/error.astro b/src/pages/error.astro index b80742e..0896bfe 100644 --- a/src/pages/error.astro +++ b/src/pages/error.astro @@ -1,5 +1,4 @@ --- -export const prerender = true; import Layout from "~/layouts/Layout.astro"; --- From 6f7018256ce34b5a638b79a3cb6fc8c9d45906af Mon Sep 17 00:00:00 2001 From: SillyCoon Date: Thu, 2 Jul 2026 19:12:55 +0900 Subject: [PATCH 7/9] add better cache key --- src/features/grammar-point/loader/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/features/grammar-point/loader/index.ts b/src/features/grammar-point/loader/index.ts index d968cb2..0a99836 100644 --- a/src/features/grammar-point/loader/index.ts +++ b/src/features/grammar-point/loader/index.ts @@ -31,9 +31,11 @@ export function grammarPointsLoader(): LiveLoader< loadCollection: async ({ filter }) => { try { if (filter && "ids" in filter) { - return grammarPointsToEntries( + return {...grammarPointsToEntries( await fetchGrammarPoints(filter.ids, filter.context), - ); + ), cacheHint: { + tags: [`grammar-points-${filter.context.user.role}-${filter.ids.join(",")}`], + }}; } const context = filter?.context ?? { user: { role: "guest" } }; From 73934058aa10a0fec1057039b88211d26e489622 Mon Sep 17 00:00:00 2001 From: SillyCoon Date: Thu, 2 Jul 2026 19:13:51 +0900 Subject: [PATCH 8/9] improve user feedback for not found gp --- src/pages/grammar/[id]/index.astro | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/grammar/[id]/index.astro b/src/pages/grammar/[id]/index.astro index e8be716..5ffd9fb 100644 --- a/src/pages/grammar/[id]/index.astro +++ b/src/pages/grammar/[id]/index.astro @@ -21,7 +21,11 @@ const { entry: grammarPointEntry, error: grammarPointError } = context: contextFromAstro(Astro), }); -if (!grammarPointEntry || grammarPointError) { +if (grammarPointError) { + return Astro.redirect("/error"); +} + +if (!grammarPointEntry) { return Astro.redirect("/grammar"); } From 2ab23c59a8c14bee819da8ec712ca4908fd91a0d Mon Sep 17 00:00:00 2001 From: SillyCoon Date: Thu, 2 Jul 2026 19:15:18 +0900 Subject: [PATCH 9/9] fix formatting --- src/features/grammar-point/loader/index.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/features/grammar-point/loader/index.ts b/src/features/grammar-point/loader/index.ts index 0a99836..7b9a6f7 100644 --- a/src/features/grammar-point/loader/index.ts +++ b/src/features/grammar-point/loader/index.ts @@ -31,11 +31,16 @@ export function grammarPointsLoader(): LiveLoader< loadCollection: async ({ filter }) => { try { if (filter && "ids" in filter) { - return {...grammarPointsToEntries( - await fetchGrammarPoints(filter.ids, filter.context), - ), cacheHint: { - tags: [`grammar-points-${filter.context.user.role}-${filter.ids.join(",")}`], - }}; + return { + ...grammarPointsToEntries( + await fetchGrammarPoints(filter.ids, filter.context), + ), + cacheHint: { + tags: [ + `grammar-points-${filter.context.user.role}-${filter.ids.join(",")}`, + ], + }, + }; } const context = filter?.context ?? { user: { role: "guest" } };