Skip to content

Commit 0f68642

Browse files
committed
Optimize mock API lookups with indexed access
1 parent 0c92939 commit 0f68642

1 file changed

Lines changed: 80 additions & 29 deletions

File tree

services/mockApi.ts

Lines changed: 80 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ import {
1818

1919
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
2020

21+
const normalizePhone = (value: string) => {
22+
const digits = value.replace(/\D/g, "");
23+
return digits.length === 10 ? digits : "";
24+
};
25+
2126
/* -------------------------------------------------------------------------- */
2227
/* Default avatars */
2328
/* -------------------------------------------------------------------------- */
@@ -155,6 +160,35 @@ let USERS_DB: Record<string, UserProfile> = {
155160
},
156161
};
157162

163+
type UserLookupIndex = {
164+
byEmail: Map<string, UserProfile>;
165+
byPhone: Map<string, UserProfile>;
166+
byUsername: Map<string, UserProfile>;
167+
};
168+
169+
const buildUserLookupIndex = (): UserLookupIndex => {
170+
const byEmail = new Map<string, UserProfile>();
171+
const byPhone = new Map<string, UserProfile>();
172+
const byUsername = new Map<string, UserProfile>();
173+
174+
for (const user of Object.values(USERS_DB)) {
175+
if (user.email) byEmail.set(user.email, user);
176+
177+
const normalizedPhone = normalizePhone(user.phone || "");
178+
if (normalizedPhone) byPhone.set(normalizedPhone, user);
179+
180+
if (user.username) byUsername.set(user.username, user);
181+
}
182+
183+
return { byEmail, byPhone, byUsername };
184+
};
185+
186+
let USER_LOOKUP_INDEX = buildUserLookupIndex();
187+
188+
const refreshUserLookupIndex = () => {
189+
USER_LOOKUP_INDEX = buildUserLookupIndex();
190+
};
191+
158192
// Initialize spot for 26/07/2025
159193
const SPOT_DATE = "2025-07-26T10:00:00";
160194
const SPOT_LOCATION = "Attibele Toll Plaza";
@@ -242,6 +276,8 @@ let INVITATIONS: Invitation[] = [
242276
},
243277
];
244278

279+
let INVITATION_BY_ID = new Map(INVITATIONS.map((invitation) => [invitation.id, invitation]));
280+
245281
// Initialize payments with real payment statuses
246282
let PAYMENTS: Payment[] = [
247283
{
@@ -302,6 +338,8 @@ let PAYMENTS: Payment[] = [
302338
},
303339
];
304340

341+
let PAYMENT_BY_ID = new Map(PAYMENTS.map((payment) => [payment.id, payment]));
342+
305343
let DRINKS: Drink[] = [];
306344
let MESSAGES: ChatMessage[] = [];
307345
let MOMENTS: Moment[] = [];
@@ -320,19 +358,19 @@ export const mockApi = {
320358
): Promise<{ user: User; profile: UserProfile }> {
321359
await delay(300);
322360

323-
// Strip formatting from phone number for comparison
324-
const cleanIdentifier = identifier.replace(/\D/g, '').length === 10 ? identifier.replace(/\D/g, '') : identifier;
325-
326-
const profile = Object.values(USERS_DB).find(
327-
(u) =>
328-
(u.email === identifier ||
329-
u.phone === cleanIdentifier ||
330-
u.username === identifier) &&
331-
u.password === password &&
332-
(!orgCode?.trim() || u.org_code === orgCode.trim())
333-
);
334-
335-
if (!profile) throw new Error("Invalid credentials");
361+
const cleanIdentifier = normalizePhone(identifier);
362+
const profile =
363+
USER_LOOKUP_INDEX.byEmail.get(identifier) ??
364+
(cleanIdentifier ? USER_LOOKUP_INDEX.byPhone.get(cleanIdentifier) : undefined) ??
365+
USER_LOOKUP_INDEX.byUsername.get(identifier);
366+
367+
if (
368+
!profile ||
369+
profile.password !== password ||
370+
(orgCode?.trim() && profile.org_code !== orgCode.trim())
371+
) {
372+
throw new Error("Invalid credentials");
373+
}
336374

337375
const user: User = {
338376
id: profile.id,
@@ -357,6 +395,7 @@ export const mockApi = {
357395
): Promise<UserProfile> {
358396
await delay(200);
359397
USERS_DB[userId] = { ...USERS_DB[userId], ...updates };
398+
refreshUserLookupIndex();
360399
return USERS_DB[userId];
361400
},
362401

@@ -370,7 +409,8 @@ export const mockApi = {
370409
}): Promise<UserProfile> {
371410
await delay(300);
372411

373-
if (Object.values(USERS_DB).some((u) => u.phone === data.phone)) {
412+
const normalizedPhone = normalizePhone(data.phone);
413+
if (normalizedPhone && USER_LOOKUP_INDEX.byPhone.has(normalizedPhone)) {
374414
throw new Error("User already exists");
375415
}
376416

@@ -390,6 +430,7 @@ export const mockApi = {
390430
};
391431

392432
USERS_DB[id] = newUser;
433+
refreshUserLookupIndex();
393434
return newUser;
394435
},
395436

@@ -425,35 +466,45 @@ export const mockApi = {
425466
if (creator && newSpot.members) {
426467
newSpot.members.push(creator);
427468

428-
INVITATIONS.push({
469+
const creatorInvitation: Invitation = {
429470
id: `inv-${Date.now()}`,
430471
spot_id: newSpot.id,
431472
user_id: creator.id,
432473
profiles: creator,
433474
status: InvitationStatus.CONFIRMED,
434-
});
475+
};
476+
INVITATIONS.push(creatorInvitation);
477+
INVITATION_BY_ID.set(creatorInvitation.id, creatorInvitation);
435478

436-
PAYMENTS.push({
479+
const creatorPayment: Payment = {
437480
id: `pay-${Date.now()}`,
438481
spot_id: newSpot.id,
439482
user_id: creator.id,
440483
profiles: creator,
441484
status: PaymentStatus.NOT_PAID,
442-
});
485+
};
486+
PAYMENTS.push(creatorPayment);
487+
PAYMENT_BY_ID.set(creatorPayment.id, creatorPayment);
443488
}
444489

445490
return newSpot;
446491
},
447492

448493
async getUpcomingSpot(): Promise<Spot | null> {
449494
await delay(200);
450-
return (
451-
SPOTS.filter((s) => new Date(s.date) >= new Date())
452-
.sort(
453-
(a, b) =>
454-
new Date(a.date).getTime() - new Date(b.date).getTime()
455-
)[0] ?? null
456-
);
495+
const now = Date.now();
496+
let nextSpot: Spot | null = null;
497+
let nextSpotTime = Number.POSITIVE_INFINITY;
498+
499+
for (const spot of SPOTS) {
500+
const spotTime = new Date(spot.date).getTime();
501+
if (spotTime >= now && spotTime < nextSpotTime) {
502+
nextSpot = spot;
503+
nextSpotTime = spotTime;
504+
}
505+
}
506+
507+
return nextSpot;
457508
},
458509

459510
async getPastSpots(): Promise<Spot[]> {
@@ -473,7 +524,7 @@ export const mockApi = {
473524
status: InvitationStatus
474525
): Promise<void> {
475526
await delay(200);
476-
const inv = INVITATIONS.find((i) => i.id === invitationId);
527+
const inv = INVITATION_BY_ID.get(invitationId);
477528
if (!inv) throw new Error("Invitation not found");
478529
inv.status = status;
479530
},
@@ -490,7 +541,7 @@ export const mockApi = {
490541
status: PaymentStatus
491542
): Promise<void> {
492543
await delay(200);
493-
const payment = PAYMENTS.find((p) => p.id === paymentId);
544+
const payment = PAYMENT_BY_ID.get(paymentId);
494545
if (!payment) throw new Error("Payment not found");
495546
payment.status = status;
496547
},
@@ -566,15 +617,15 @@ export const mockApi = {
566617

567618
async sendOtp(email: string): Promise<void> {
568619
await delay(300);
569-
const user = Object.values(USERS_DB).find((u) => u.email === email);
620+
const user = USER_LOOKUP_INDEX.byEmail.get(email);
570621
if (!user) throw new Error("User not found");
571622
// In a real implementation, this would send an OTP
572623
// For now, we'll just simulate success
573624
},
574625

575626
async resetPassword(email: string, newPassword: string): Promise<void> {
576627
await delay(300);
577-
const user = Object.values(USERS_DB).find((u) => u.email === email);
628+
const user = USER_LOOKUP_INDEX.byEmail.get(email);
578629
if (!user) throw new Error("User not found");
579630
user.password = newPassword;
580631
},

0 commit comments

Comments
 (0)