From 0f920688412d9833faf9a53a58fa9d5f21513ae9 Mon Sep 17 00:00:00 2001 From: "M. E. Abdelsalam" Date: Thu, 26 Dec 2024 04:56:06 +0200 Subject: [PATCH] removed: rules from the cache; and its corresponding uses across the code base. --- packages/luna/src/locales/ar-eg.json | 3 +- packages/models/src/cache/index.ts | 3 - packages/models/src/cache/rules.ts | 85 ------------------------ packages/models/tests/cache/rule.test.ts | 73 -------------------- packages/types/src/rule.ts | 6 -- packages/types/src/tutor.ts | 2 +- services/server/src/handlers/lesson.ts | 6 +- services/server/src/handlers/rule.ts | 49 +------------- services/server/src/handlers/user.ts | 27 +++----- services/server/src/lib/events.ts | 36 ---------- services/server/src/lib/tutor.ts | 68 +++---------------- services/server/tests/api/user.test.ts | 1 - 12 files changed, 26 insertions(+), 333 deletions(-) delete mode 100644 packages/models/src/cache/rules.ts delete mode 100644 packages/models/tests/cache/rule.test.ts delete mode 100644 services/server/src/lib/events.ts diff --git a/packages/luna/src/locales/ar-eg.json b/packages/luna/src/locales/ar-eg.json index a0ba2145..64f58927 100644 --- a/packages/luna/src/locales/ar-eg.json +++ b/packages/luna/src/locales/ar-eg.json @@ -494,7 +494,6 @@ "student-dashboard.overview.total-lessons": "اجمالي الحصص", "student-dashboard.overview.completed-lessons": "الحصص المكتملة", "student-dashboard.overview.total-learning-time": "اجمالي وقت التعلم", - "student-dashboard.overview.total-learning-time.unit.hour": "{value} س", "student-dashboard.overview.total-learning-time.unit.minute": "{value} د", "student-dashboard.overview.teachers": "عدد المعلمين", "student-dashboard.loading": "برجاء الانتظار... جاري تحميل بيانات لوحتك الرئيسية!", @@ -718,4 +717,4 @@ "error.password.short": "رقم سري قصير", "error.password.long": "رقم سري طويل", "error.password.invalid": "يجب أن يحتوي علي أرقام، حروف ، رموز" -} +} \ No newline at end of file diff --git a/packages/models/src/cache/index.ts b/packages/models/src/cache/index.ts index b19e70df..fe88d06b 100644 --- a/packages/models/src/cache/index.ts +++ b/packages/models/src/cache/index.ts @@ -1,6 +1,5 @@ import { createClient } from "redis"; import { Tutors } from "@/cache/tutors"; -import { Rules } from "@/cache/rules"; import { RedisClient } from "@/cache/base"; import { Peer } from "@/cache/peer"; import { Call } from "@/cache/call"; @@ -8,7 +7,6 @@ import { OnlineStatus } from "@/cache/onlineStatus"; export class Cache { public tutors: Tutors; - public rules: Rules; public peer: Peer; public call: Call; public onlineStatus: OnlineStatus; @@ -18,7 +16,6 @@ export class Cache { const client = createClient({ url }); this.client = client; this.tutors = new Tutors(client); - this.rules = new Rules(client); this.peer = new Peer(client); this.call = new Call(client); this.onlineStatus = new OnlineStatus(client); diff --git a/packages/models/src/cache/rules.ts b/packages/models/src/cache/rules.ts deleted file mode 100644 index f46b349a..00000000 --- a/packages/models/src/cache/rules.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { CacheBase } from "@/cache/base"; -import { IRule } from "@litespace/types"; -import { isEmpty } from "lodash"; - -/** - * Rules cache - * - * @data map from rule id to the unpacked rule events. - */ -export class Rules extends CacheBase { - readonly key = "rules"; - readonly ttl = 60 * 60 * 24; // 24 hours - - async setOne({ tutor, rule, events }: IRule.Cache) { - const exists = await this.exists(); - const filed = this.asField({ tutor, rule }); - const value = this.encode(events); - - if (exists) return await this.client.hSet(this.key, filed, value); - - await this.client - .multi() - .hSet(this.key, filed, value) - .expire(this.key, this.ttl) - .exec(); - } - - async getOne({ - tutor, - rule, - }: { - tutor: number; - rule: number; - }): Promise { - const result = await this.client.hGet( - this.key, - this.asField({ tutor, rule }) - ); - if (!result) return null; - return this.decode(result); - } - - async deleteOne({ tutor, rule }: { tutor: number; rule: number }) { - await this.client.hDel(this.key, this.asField({ tutor, rule })); - } - - async setMany(payload: IRule.Cache[]) { - const cache: Record = {}; - for (const { tutor, rule, events } of payload) { - cache[this.asField({ tutor, rule })] = this.encode(events); - } - - if (isEmpty(cache)) return; - await this.client - .multi() - .hSet(this.key, cache) - .expire(this.key, this.ttl) - .exec(); - } - - async getAll(): Promise { - const result = await this.client.hGetAll(this.key); - const rules: IRule.Cache[] = []; - - for (const [key, value] of Object.entries(result)) { - const [tutor, rule] = key.split(":"); - rules.push({ - tutor: Number(tutor), - rule: Number(rule), - events: this.decode(value), - }); - } - - return rules; - } - - async exists(): Promise { - const output = await this.client.exists(this.key); - return !!output; - } - - asField({ tutor, rule }: { tutor: number; rule: number }): string { - return `${tutor}:${rule}`; - } -} diff --git a/packages/models/tests/cache/rule.test.ts b/packages/models/tests/cache/rule.test.ts deleted file mode 100644 index c6ecd35c..00000000 --- a/packages/models/tests/cache/rule.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { cache } from "@fixtures/cache"; -import { expect } from "chai"; -import dayjs from "dayjs"; -import { first } from "lodash"; - -describe("Testing cache/rules functions", () => { - beforeAll(async () => { - await cache.connect(); - }); - - afterAll(async () => { - await cache.disconnect(); - }); - - beforeEach(async () => { - await cache.flush(); - }); - - it("should set/retreive one rule in/from the cache", async () => { - await cache.rules.setOne({ tutor: 1, rule: 1, events: [] }) - let res = await cache.rules.getOne({ tutor: 1, rule: 1 }) - expect(res).to.have.length(0); - - const mockRuleEvent = { - id: 1, - start: dayjs().toISOString(), - end: dayjs().add(1, "hour").toISOString(), - } - - await cache.rules.setOne({ tutor: 1, rule: 1, events: [mockRuleEvent] }); - res = await cache.rules.getOne({ tutor: 1, rule: 1 }) - expect(res).to.have.length(1); - expect(first(res)).to.deep.eq(mockRuleEvent); - }); - - it("should set/retreive many rules in/from the cache", async () => { - const mockRuleEvents = [ - { - id: 1, - start: dayjs().toISOString(), - end: dayjs().add(1, "hour").toISOString(), - }, - { - id: 2, - start: dayjs().add(1, "day").toISOString(), - end: dayjs().add(2, "days").toISOString(), - }, - ] - - const mock1 = { tutor: 1, rule: 1, events: [mockRuleEvents[0]] }; - const mock2 = { tutor: 2, rule: 2, events: [mockRuleEvents[1]] }; - - await cache.rules.setMany([mock1, mock2]); - - const res = await cache.rules.getAll() - expect(res).to.have.length(2); - expect(res).to.deep.contains(mock1); - expect(res).to.deep.contains(mock2); - }); - - it("should remove one rule from the cache", async () => { - await cache.rules.setOne({ tutor: 1, rule: 1, events: [] }) - await cache.rules.deleteOne({ tutor: 1, rule: 1 }); - const res = await cache.rules.getOne({ tutor: 1, rule: 1 }); - expect(res).to.eq(null); - }); - - it("should check if rules key does exist in the cache", async () => { - expect(await cache.rules.exists()).to.false; - await cache.rules.setOne({ tutor: 1, rule: 1, events: [] }) - expect(await cache.rules.exists()).to.true; - }); -}); diff --git a/packages/types/src/rule.ts b/packages/types/src/rule.ts index 5bbe84ff..07ac4c52 100644 --- a/packages/types/src/rule.ts +++ b/packages/types/src/rule.ts @@ -97,12 +97,6 @@ export type FindUserRulesWithSlotsApiResponse = { slots: Slot[]; }; -export type Cache = { - tutor: number; - rule: number; - events: RuleEvent[]; -}; - /** * Slot: represents unpacked time slot at specific time and duration. * diff --git a/packages/types/src/tutor.ts b/packages/types/src/tutor.ts index 47c14219..f6fd815c 100644 --- a/packages/types/src/tutor.ts +++ b/packages/types/src/tutor.ts @@ -91,7 +91,7 @@ export type FindTutorInfoApiResponse = { export type FindOnboardedTutorsApiResponse = { total: number; - list: Array; + list: Array; }; export type PublicTutorFieldsForStudio = { diff --git a/services/server/src/handlers/lesson.ts b/services/server/src/handlers/lesson.ts index dbef962b..b397d8f7 100644 --- a/services/server/src/handlers/lesson.ts +++ b/services/server/src/handlers/lesson.ts @@ -20,7 +20,6 @@ import { safe } from "@litespace/sol/error"; import { unpackRules } from "@litespace/sol/rule"; import { isAdmin, isStudent, isUser } from "@litespace/auth"; import { platformConfig } from "@/constants"; -import { cache } from "@/lib/cache"; import dayjs from "@/lib/dayjs"; import { canBook } from "@/lib/call"; import { concat, isEqual } from "lodash"; @@ -124,8 +123,6 @@ function create(context: ApiContext) { }), }; - await cache.rules.setOne(payload); - context.io.sockets .in(Wss.Room.TutorsCache) .emit(Wss.ServerEvent.LessonBooked, payload); @@ -253,8 +250,7 @@ function cancel(context: ApiContext) { slots, }), }; - // update tutor rules cache - await cache.rules.setOne(payload); + // notify client context.io.sockets .to(Wss.Room.TutorsCache) diff --git a/services/server/src/handlers/rule.ts b/services/server/src/handlers/rule.ts index f83200e2..eab0a85b 100644 --- a/services/server/src/handlers/rule.ts +++ b/services/server/src/handlers/rule.ts @@ -20,10 +20,8 @@ import { asSlots, unpackRules, } from "@litespace/sol/rule"; -import { safe } from "@litespace/sol/error"; import { isEmpty } from "lodash"; import { ApiContext } from "@/types/api"; -import { cache } from "@/lib/cache"; import dayjs from "@/lib/dayjs"; import { isTutorManager, isTutor, isUser } from "@litespace/auth"; import { isOnboard } from "@/lib/tutor"; @@ -86,22 +84,7 @@ function createRule(context: ApiContext) { } const rule = await rules.create({ ...payload, userId: user.id }); res.status(201).json(rule); - - const error = await safe(async () => { - const today = dayjs.utc().startOf("day"); - await cache.rules.setOne({ - tutor: rule.userId, - rule: rule.id, - events: unpackRules({ - rules: [rule], - slots: [], - start: today.toISOString(), - end: today.add(30, "days").toISOString(), - }), - }); - context.io.to(Wss.Room.TutorsCache).emit(Wss.ServerEvent.RuleCreated); - }); - if (error instanceof Error) console.log(error); + context.io.to(Wss.Room.TutorsCache).emit(Wss.ServerEvent.RuleCreated); } ); } @@ -256,28 +239,7 @@ function updateRule(context: ApiContext) { activated: payload.activated, }); res.status(200).json(updatedRule); - - const error = await safe(async () => { - const today = dayjs.utc().startOf("day"); - const ruleLessons = await lessons.find({ - rules: [rule.id], - canceled: false, - full: true, - }); - - await cache.rules.setOne({ - tutor: rule.userId, - rule: rule.id, - events: unpackRules({ - rules: [rule], - slots: asSlots(ruleLessons.list), - start: today.toISOString(), - end: today.add(30, "days").toISOString(), - }), - }); - context.io.to(Wss.Room.TutorsCache).emit(Wss.ServerEvent.RuleUpdated); - }); - if (error instanceof Error) console.log(error); + context.io.to(Wss.Room.TutorsCache).emit(Wss.ServerEvent.RuleUpdated); } ); } @@ -308,12 +270,7 @@ function deleteRule(context: ApiContext) { : await rules.update(ruleId, { deleted: true }); res.status(204).json(deletedRule); - - const error = await safe(async () => { - await cache.rules.deleteOne({ tutor: rule.userId, rule: rule.id }); - context.io.to(Wss.Room.TutorsCache).emit(Wss.ServerEvent.RuleDeleted); - }); - if (error instanceof Error) console.log(error); + context.io.to(Wss.Room.TutorsCache).emit(Wss.ServerEvent.RuleDeleted); } ); } diff --git a/services/server/src/handlers/user.ts b/services/server/src/handlers/user.ts index 9b106b94..4fd8e28b 100644 --- a/services/server/src/handlers/user.ts +++ b/services/server/src/handlers/user.ts @@ -58,7 +58,6 @@ import { cache } from "@/lib/cache"; import { sendBackgroundMessage } from "@/workers"; import { WorkerMessageType } from "@/workers/messages"; import { isValidPassword } from "@litespace/sol/verification"; -import { selectTutorRuleEvents } from "@/lib/events"; import { isTutor, isTutorManager } from "@litespace/auth/dist/authorization"; const createUserPayload = zod.object({ @@ -391,21 +390,13 @@ async function findOnboardedTutors(req: Request, res: Response) { const query: ITutor.FindOnboardedTutorsParams = findOnboardedTutorsQuery.parse(req.query); - const [isTutorsCached, isRulesCached] = await Promise.all([ - cache.tutors.exists(), - cache.rules.exists(), - ]); - - const validCacheState = isTutorsCached && isRulesCached; + const isTutorsCached = await cache.tutors.exists(); // retrieve/set tutors and rules from/in cache (redis) - const { tutors, rules } = validCacheState - ? { - tutors: await cache.tutors.getAll(), - rules: await cache.rules.getAll(), - } - : // DONE: Update the tutors cache according to the new design in (@/architecture/v1.0/tutors.md) - await cacheTutors(dayjs.utc().startOf("day")); + const tutorsCache = isTutorsCached + ? await cache.tutors.getAll() + // DONE: Update the tutors cache according to the new design in (@/architecture/v1.0/tutors.md) + : await cacheTutors(); // order tutors based on time of the first event, genger of the user // online state, and notice. @@ -414,17 +405,17 @@ async function findOnboardedTutors(req: Request, res: Response) { isUser(user) && user.gender ? (user.gender as IUser.Gender) : undefined; const filtered = query.search - ? tutors.filter((tutor) => { + ? tutorsCache.filter((tutor) => { if (!query.search) return true; const regex = new RegExp(query.search, "gi"); const nameMatch = tutor.name && regex.test(tutor.name); const topicMatch = tutor.topics.find((topic) => regex.test(topic)); return nameMatch || topicMatch; }) - : tutors; + : tutorsCache; + const ordered = orderTutors({ tutors: filtered, - rules, userGender, }); @@ -439,7 +430,7 @@ async function findOnboardedTutors(req: Request, res: Response) { // ITutor.FindOnboardedTutorsApiResponse list attribute const list = paginated.map((tutor) => ({ ...tutor, - rules: selectTutorRuleEvents(rules, tutor), + slots: [] // TODO: retrieve AvailabilitySlots from the db })); // DONE: Update the response to match the new design in (@/architecture/v1.0/tutors.md) diff --git a/services/server/src/lib/events.ts b/services/server/src/lib/events.ts deleted file mode 100644 index 207068f0..00000000 --- a/services/server/src/lib/events.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ILesson, IRule, ITutor } from "@litespace/types"; -import { flatten } from "lodash"; -import { Schedule } from "@litespace/sol/rule"; -import dayjs from "@/lib/dayjs"; - -/* - * returns IRule.RuleEvent objects from IRule.Cache for a specific tutor. - * returned events are filtered to contain only bookable events. - */ -export function selectTutorRuleEvents( - rules: IRule.Cache[], - tutor: ITutor.Cache -): IRule.RuleEvent[] { - const now = dayjs.utc(); - - const tutorRules = rules.filter((rule) => rule.tutor === tutor.id); - const events = flatten(tutorRules.map((rule) => rule.events)).filter( - (event) => { - const adjustedNow = now.add(tutor.notice, "minutes"); - const start = dayjs.utc(event.start); - const same = start.isSame(adjustedNow); - const after = start.isAfter(adjustedNow); - // NOTE: users can book after the start of the event - const between = adjustedNow.isBetween( - event.start, - // rule should have some time suitable for booking at least one short lesson. - dayjs.utc(event.end).subtract(ILesson.Duration.Short, "minutes"), - "minute", - "[]" - ); - return same || after || between; - } - ); - - return Schedule.order(events, "asc"); -} diff --git a/services/server/src/lib/tutor.ts b/services/server/src/lib/tutor.ts index 7e185cf0..2f6450c1 100644 --- a/services/server/src/lib/tutor.ts +++ b/services/server/src/lib/tutor.ts @@ -1,33 +1,22 @@ -import { IRule, ITutor } from "@litespace/types"; -import dayjs, { Dayjs } from "dayjs"; +import { ITutor } from "@litespace/types"; import { Knex } from "knex"; import { - rules, tutors, knex, lessons, topics, ratings, } from "@litespace/models"; -import { entries, first, flatten, groupBy, orderBy } from "lodash"; -import { unpackRules } from "@litespace/sol/rule"; +import { first, orderBy } from "lodash"; import { cache } from "@/lib/cache"; -import { selectTutorRuleEvents } from "./events"; import { Gender } from "@litespace/types/dist/esm/user"; -export type TutorsCache = { - tutors: ITutor.Cache[]; - rules: IRule.Cache[]; -}; +export type TutorsCache = ITutor.Cache[]; -export async function constructTutorsCache(date: Dayjs): Promise { +export async function constructTutorsCache(): Promise { // create the cache for the next month starting from `date` - const start = date.startOf("day"); - const end = start.add(30, "days").endOf("day"); - const [ onboardedTutors, - tutorsRules, tutorsTopics, tutorsRatings, tutorsLessonsCount, @@ -38,7 +27,6 @@ export async function constructTutorsCache(date: Dayjs): Promise { return await Promise.all([ onboardedTutors, - rules.findActivatedRules(tutorIds, start.toISOString(), tx), topics.findUserTopics({ users: tutorIds }), ratings.findAvgRatings(tutorIds), lessons.countLessonsBatch({ users: tutorIds, canceled: false }), @@ -49,27 +37,8 @@ export async function constructTutorsCache(date: Dayjs): Promise { ]); }); - //! todo: filter lessons by tutor id - // const tutorCallsMap = groupBy(tutorLessons, l => l.); - const tutorRulesMap = groupBy(tutorsRules, "userId"); - const rulesCachePayload = entries(tutorRulesMap).map( - ([tutor, rules]): IRule.Cache[] => { - // const calls = tutorCallsMap[tutor] || []; - return rules.map((rule) => ({ - tutor: Number(tutor), - rule: rule.id, - events: unpackRules({ - rules: [rule], - slots: [], - start: start.toISOString(), - end: end.toISOString(), - }), - })); - } - ); - // restruct tutors list to match ITutor.Cache[] - const cacheTutors: ITutor.Cache[] = onboardedTutors.map((tutor) => { + const tutorsCache: ITutor.Cache[] = onboardedTutors.map((tutor) => { const filteredTopics = tutorsTopics ?.filter((item) => item.userId === tutor.id) .map((item) => item.name.ar); @@ -94,19 +63,13 @@ export async function constructTutorsCache(date: Dayjs): Promise { }; }); - return { - tutors: cacheTutors, - rules: flatten(rulesCachePayload), - }; + return tutorsCache; } -export async function cacheTutors(start: Dayjs): Promise { - const cachePayload = await constructTutorsCache(start); - await Promise.all([ - cache.tutors.setMany(cachePayload.tutors), - cache.rules.setMany(cachePayload.rules), - ]); - return cachePayload; +export async function cacheTutors(): Promise { + const tutorsCache = await constructTutorsCache(); + await cache.tutors.setMany(tutorsCache); + return tutorsCache; } /** @@ -129,21 +92,12 @@ export function isPublicTutor( */ export function orderTutors({ tutors, - rules, userGender, }: { tutors: ITutor.Cache[]; - rules: IRule.Cache[]; userGender?: Gender; }): ITutor.Cache[] { const iteratees = [ - // sort in ascending order by the first availablity - (tutor: ITutor.Cache) => { - const events = selectTutorRuleEvents(rules, tutor); - const event = first(events); - if (!event) return Infinity; - return dayjs.utc(event.start).unix(); - }, (tutor: ITutor.Cache) => { if (!userGender) return 0; // disable ordering by gender if user is not logged in or gender is unkown if (!tutor.gender) return Infinity; @@ -152,7 +106,7 @@ export function orderTutors({ }, "notice", ]; - const orders: Array<"asc" | "desc"> = ["asc", "asc", "asc"]; + const orders: Array<"asc" | "desc"> = ["asc", "asc"]; return orderBy(tutors, iteratees, orders); } diff --git a/services/server/tests/api/user.test.ts b/services/server/tests/api/user.test.ts index 4556ba26..3e4c035a 100644 --- a/services/server/tests/api/user.test.ts +++ b/services/server/tests/api/user.test.ts @@ -88,7 +88,6 @@ describe("/api/v1/user/", () => { it("should successfully load onboard tutors from db to cache", async () => { expect(await cache.tutors.exists()).to.eql(false); - expect(await cache.rules.exists()).to.eql(false); const newUser = await db.user({ role: Role.SuperAdmin }); const newTutor = await db.tutor();