From 519a8ba0eda7abbe560917313d7623c6f109afb6 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/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 | 15 ++--- services/server/src/lib/events.ts | 36 ---------- services/server/src/lib/tutor.ts | 38 ++--------- services/server/tests/api/user.test.ts | 1 - 11 files changed, 17 insertions(+), 297 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/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 14aa85fc..ffcc626e 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..02f5fb2c 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; // TODO: user AvailablitySlot.Self }; 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..f1c48426 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,18 +390,14 @@ 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 [isTutorsCached] = await Promise.all([ cache.tutors.exists() ]); - const validCacheState = isTutorsCached && isRulesCached; + const validCacheState = isTutorsCached; // retrieve/set tutors and rules from/in cache (redis) - const { tutors, rules } = validCacheState + const { tutors } = 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")); @@ -424,7 +419,7 @@ async function findOnboardedTutors(req: Request, res: Response) { : tutors; const ordered = orderTutors({ tutors: filtered, - rules, + slots: [], // TODO: retrieve AvailabilitySlots from db userGender, }); @@ -439,7 +434,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..0dbeeeda 100644 --- a/services/server/src/lib/tutor.ts +++ b/services/server/src/lib/tutor.ts @@ -9,15 +9,12 @@ import { 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 async function constructTutorsCache(date: Dayjs): Promise { @@ -49,25 +46,6 @@ 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 filteredTopics = tutorsTopics @@ -96,7 +74,6 @@ export async function constructTutorsCache(date: Dayjs): Promise { return { tutors: cacheTutors, - rules: flatten(rulesCachePayload), }; } @@ -104,7 +81,6 @@ 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; } @@ -129,20 +105,20 @@ export function isPublicTutor( */ export function orderTutors({ tutors, - rules, + slots, userGender, }: { tutors: ITutor.Cache[]; - rules: IRule.Cache[]; + slots: IRule.Self[]; // TODO: substitute IRule with IAvailabilitySlot 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(); + const filtered = slots.filter(slot => slot.userId === tutor.id); + const slot = first(filtered); + if (!slot) return Infinity; + return dayjs.utc(slot.start).unix(); }, (tutor: ITutor.Cache) => { if (!userGender) return 0; // disable ordering by gender if user is not logged in or gender is unkown 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();