Skip to content

Commit 8ced025

Browse files
committed
#74 introduced speakers-allInOne firestore doc
to avoid making 280+ queries on devoxxfr speakers directory screen
1 parent 5ffd729 commit 8ced025

File tree

7 files changed

+145
-34
lines changed

7 files changed

+145
-34
lines changed

cloud/firestore/firestore.default.rules

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ service cloud.firestore {
9797
allow get, list: if true;
9898
allow write: if false;
9999
}
100+
match /speakers-allInOne/self {
101+
allow get: if true;
102+
allow list, write: if false;
103+
}
100104

101105
match /organizer-space/{secretOrganizerToken} {
102106
allow get: if true;
@@ -165,6 +169,10 @@ service cloud.firestore {
165169
allow get: if true;
166170
allow list, write: if false;
167171
}
172+
match /speakers-allInOne/self {
173+
allow get: if true;
174+
allow list, write: if false;
175+
}
168176
match /speakers/{speakerId} {
169177
allow get, list: if true;
170178
allow write: if false;

cloud/firestore/firestore.default.rules.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,24 @@ const FIREBASE_MANAGED_COLLECTIONS = [
391391
}
392392
}),
393393
}]
394+
}, {
395+
name: '/events/{eventId}/speakers-allInOne/self',
396+
docInitializations: [{
397+
name: 'default',
398+
collection: '/events/an-event/speakers-allInOne',
399+
path: '/events/an-event/speakers-allInOne/self',
400+
newDocPath: '/events/an-event/speakers-allInOne/other',
401+
data: () => ({
402+
"12345": {
403+
id: `12345`,
404+
}
405+
}),
406+
updatedData: () => ({
407+
"54321": {
408+
id: `54321`,
409+
}
410+
}),
411+
}]
394412
}, {
395413
name: '/events/{eventId}/speakers/{speakerId}',
396414
docInitializations: [{
@@ -569,6 +587,24 @@ const FIREBASE_MANAGED_COLLECTIONS = [
569587
}
570588
}),
571589
}]
590+
}, {
591+
name: '/spaces/{spaceId}/events/{eventId}/speakers-allInOne/self',
592+
docInitializations: [{
593+
name: 'default',
594+
collection: '/spaces/12345678-1234-5678-90ab-1234567890ab/events/an-event/speakers-allInOne',
595+
path: '/spaces/12345678-1234-5678-90ab-1234567890ab/events/an-event/speakers-allInOne/self',
596+
newDocPath: '/spaces/12345678-1234-5678-90ab-1234567890ab/events/an-event/speakers-allInOne/other',
597+
data: () => ({
598+
"12345": {
599+
id: `12345`,
600+
}
601+
}),
602+
updatedData: () => ({
603+
"54321": {
604+
id: `54321`,
605+
}
606+
}),
607+
}]
572608
}, {
573609
name: '/spaces/{spaceId}/events/{eventId}/speakers/{speakerId}',
574610
docInitializations: [{
@@ -1185,6 +1221,20 @@ const COLLECTIONS: CollectionDescriptor[] = [{
11851221
})
11861222
},
11871223
spaceId: space.id,
1224+
}, {
1225+
name: `/${eventFirestorePath(space.id)}/speakers-allInOne`,
1226+
aroundTests: (_: UserContext) => ({
1227+
beforeEach: [],
1228+
afterEach: [],
1229+
}),
1230+
tests: (userContext: UserContext) => {
1231+
ensureCollectionFollowAccessPermissions(`/${eventFirestorePath(space.id)}/speakers-allInOne/self`, userContext,
1232+
{
1233+
get: true,
1234+
list: false, write: false
1235+
})
1236+
},
1237+
spaceId: space.id,
11881238
}, {
11891239
name: `/${eventFirestorePath(space.id)}/speakers`,
11901240
aroundTests: (_: UserContext) => ({
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {createAllSpeakers, getAllEventsDocs} from "../services/event-utils";
2+
import {getEventTalks} from "../services/talk-utils";
3+
4+
export async function introduceSpeakersAllInOneDoc(): Promise<"OK"|"Error"> {
5+
const eventDocs = await getAllEventsDocs({includePrivateSpaces: true})
6+
await Promise.all([
7+
...eventDocs.map(async eventDoc => {
8+
try {
9+
const eventTalks = await getEventTalks(eventDoc.ref, eventDoc.id);
10+
11+
const event = eventDoc.data();
12+
const { createdSpeakers } = await createAllSpeakers(eventTalks, event.visibility === 'private' ? event.spaceToken : undefined, event.id);
13+
console.log(`${createdSpeakers.length} event speakers re-created for event id ${eventDoc.id}`)
14+
} catch (err) {
15+
console.error(`Error during Event speaker reset for ${eventDoc.id}: ${err}`)
16+
}
17+
}),
18+
])
19+
20+
return "OK"
21+
}

cloud/functions/src/functions/firestore/services/event-utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {AllInOneTalkStats} from "../../../../../../shared/event-stats";
99
import {detailedTalksToSpeakersLineup} from "../../../models/Event";
1010
import {DetailedTalk} from "../../../../../../shared/daily-schedule.firestore";
1111
import {toValidFirebaseKey} from "../../../../../../shared/utilities/firebase.utils";
12+
import {LineupSpeaker} from "../../../../../../shared/event-lineup.firestore";
1213
import {arrayDiff} from "../../../../../../shared/utilities/arrays.utils";
1314

1415

@@ -64,5 +65,12 @@ export async function createAllSpeakers(eventTalks: DetailedTalk[], maybeSpaceTo
6465
...speakersDiff.elementsAlreadyPresent.map(async ({ origin: speakerDoc, target: lineupSpeaker }) => speakerDoc.update(lineupSpeaker)),
6566
])
6667

68+
const allInOneSpeakers = lineupSpeakers.reduce((allInOneSpeakers, lineupSpeaker) => {
69+
allInOneSpeakers[lineupSpeaker.id] = lineupSpeaker;
70+
return allInOneSpeakers;
71+
}, {} as Record<string, LineupSpeaker>)
72+
73+
await db.doc(`${eventRootPath}/speakers-allInOne/self`).set(allInOneSpeakers);
74+
6775
return { createdSpeakers: lineupSpeakers }
6876
}

cloud/functions/src/functions/http/migrateFirestoreSchema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const MIGRATIONS: Migration[] = [
3737
{ name: "fillTalkEditorsSpeakersAndTitle", exec: async () => (await import("../firestore/migrations/026-fillTalkEditorsSpeakersAndTitle")).fillTalkEditorsSpeakersAndTitle() },
3838
{ name: "introduceEventSpeakersAndFixTalkUndefinedTags", exec: async () => (await import("../firestore/migrations/027-introduceEventSpeakersAndFixTalkUndefinedTags")).introduceEventSpeakersAndFixTalkUndefinedTags() },
3939
{ name: "introduceDetailedTalksAllocation", exec: async () => (await import("../firestore/migrations/028-introduceDetailedTalksAllocation")).introduceDetailedTalksAllocation() },
40+
{ name: "introduceSpeakersAllInOneDoc", exec: async () => (await import("../firestore/migrations/029-introduceSpeakersAllInOneDoc")).introduceSpeakersAllInOneDoc() },
4041
];
4142

4243
export type MigrationResult = "OK"|"Error";

mobile/src/state/useEventSpeakers.ts

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,37 @@
11
import {computed, Ref, toValue} from "vue";
2-
import {deferredVuefireUseCollection, deferredVuefireUseDocument} from "@/views/vue-utils";
3-
import {VoxxrinConferenceDescriptor, VoxxrinLanguaceCode} from "@/models/VoxxrinConferenceDescriptor";
4-
import {collection, CollectionReference, doc, DocumentReference, getDocs } from "firebase/firestore";
2+
import {deferredVuefireUseDocument} from "@/views/vue-utils";
3+
import {VoxxrinConferenceDescriptor} from "@/models/VoxxrinConferenceDescriptor";
4+
import {doc, DocumentReference, getDoc} from "firebase/firestore";
55
import {db} from "@/state/firebase";
66
import {resolvedEventFirestorePath} from "../../../shared/utilities/event-utils";
77
import {LineupSpeaker} from "../../../shared/event-lineup.firestore";
88
import {createVoxxrinSpeakerFromFirestore, SpeakerId, speakerMatchesSearchTerms} from "@/models/VoxxrinSpeaker";
9-
import {match} from "ts-pattern";
109
import {CompletablePromiseQueue, sortBy} from "@/models/utils";
1110
import {User} from "firebase/auth";
1211
import {checkCache} from "@/services/Cachings";
1312
import {Temporal} from "temporal-polyfill";
1413
import {PERF_LOGGER} from "@/services/Logger";
1514
import {loadSpeakerUrl} from "@/state/useEventTalk";
1615
import {toValidFirebaseKey} from "../../../shared/utilities/firebase.utils";
16+
import {match, P} from "ts-pattern";
1717

1818
export function useLineupSpeakers(eventDescriptorRef: Ref<VoxxrinConferenceDescriptor|undefined>, searchTermsRef: Ref<string|undefined>) {
1919

20-
const firestoreSpeakersRef = deferredVuefireUseCollection([ eventDescriptorRef ],
21-
([eventDescriptor]) => eventLineupSpeakersCollections(eventDescriptor),
22-
(firestoreSpeaker: LineupSpeaker) => firestoreSpeaker,
23-
() => {},
24-
(change, speakerId, collectionRef) => {
25-
match(change)
26-
.with({type:'created'}, change => collectionRef.value.set(speakerId, change.createdDoc))
27-
.with({type:'updated'}, change => collectionRef.value.set(speakerId, change.updatedDoc))
28-
.with({type:'deleted'}, change => collectionRef.value.delete(speakerId))
29-
.exhaustive()
30-
}
31-
);
20+
const firestoreAllSpeakersRef = deferredVuefireUseDocument([eventDescriptorRef],
21+
([maybeEventDescriptor]) => allEventLineupSpeakersDoc(maybeEventDescriptor));
3222

3323
return {
3424
speakers: computed(() => {
35-
const firestoreSpeakersLineup = toValue(firestoreSpeakersRef),
25+
const firestoreAllSpeakersLineup = toValue(firestoreAllSpeakersRef),
3626
eventDescriptor = toValue(eventDescriptorRef),
3727
searchTerms = toValue(searchTermsRef);
3828

39-
if(!firestoreSpeakersLineup || !eventDescriptor) {
40-
return undefined;
29+
if(!firestoreAllSpeakersLineup || !eventDescriptor) {
30+
return [];
4131
}
4232

4333
const speakers = sortBy(
44-
[...firestoreSpeakersLineup.values()]
34+
[...Object.values(firestoreAllSpeakersLineup)]
4535
.map(fSpeaker => createVoxxrinSpeakerFromFirestore(eventDescriptor, fSpeaker))
4636
.filter(speaker => speakerMatchesSearchTerms(speaker, searchTerms)),
4737
sp => sp.fullName
@@ -71,16 +61,12 @@ export function useLineupSpeaker(eventDescriptorRef: Ref<VoxxrinConferenceDescri
7161
}
7262
}
7363

74-
export function eventLineupSpeakersCollections(eventDescriptor: VoxxrinConferenceDescriptor|undefined) {
75-
if(!eventDescriptor || !eventDescriptor.id || !eventDescriptor.id.value) {
76-
return [];
64+
export function allEventLineupSpeakersDoc(maybeEventDescriptor: VoxxrinConferenceDescriptor|undefined) {
65+
if(!maybeEventDescriptor || !maybeEventDescriptor.id || !maybeEventDescriptor.id.value) {
66+
return undefined;
7767
}
7868

79-
return [
80-
collection(db,
81-
`${resolvedEventFirestorePath(eventDescriptor.id.value, eventDescriptor.spaceToken?.value)}/speakers`
82-
) as CollectionReference<LineupSpeaker>
83-
];
69+
return doc(db, `${resolvedEventFirestorePath(maybeEventDescriptor.id.value, maybeEventDescriptor.spaceToken?.value)}/speakers-allInOne/self`) as DocumentReference<Record<string, LineupSpeaker>>;
8470
}
8571

8672
export function eventLineupSpeakerDocument(eventDescriptor: VoxxrinConferenceDescriptor|undefined, speakerId: SpeakerId|undefined) {
@@ -99,13 +85,14 @@ export async function prepareEventSpeakers(user: User, conferenceDescriptor: Vox
9985
async () => {
10086
PERF_LOGGER.debug(`eventTalkPreparation(eventId=${conferenceDescriptor.id.value})`)
10187

102-
const speakersColl = eventLineupSpeakersCollections(conferenceDescriptor)[0];
103-
const speakers = await getDocs(speakersColl)
88+
const maybeAllSpeakersDoc = allEventLineupSpeakersDoc(conferenceDescriptor);
89+
const allSpeakersById = await match(maybeAllSpeakersDoc)
90+
.with(P.nullish, async () => ({} as Record<string, LineupSpeaker>))
91+
.otherwise(async allSpeakersDoc => (await getDoc(allSpeakersDoc)).data() || {});
10492

105-
promisesQueue.addAll(speakers.docs.map(speaker => () => {
106-
const speakerData = speaker.data()
107-
if(speakerData.photoUrl) {
108-
return loadSpeakerUrl(speakerData.photoUrl);
93+
promisesQueue.addAll(Object.values(allSpeakersById).map(speaker => () => {
94+
if(speaker.photoUrl) {
95+
return loadSpeakerUrl(speaker.photoUrl);
10996
}
11097
}), {priority: 100 });
11198
}), { priority: 1000 });

shared/utilities/arrays.utils.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,39 @@ export function dedupe<T>(array: T[], hashExtractor: (elem: T) => string): T[] {
1010
}
1111
return result;
1212
}
13+
14+
export type ArrayDiff<ORIGIN, TARGET> = {
15+
elementsToAdd: TARGET[],
16+
elementsToRemove: ORIGIN[],
17+
elementsAlreadyPresent: { origin: ORIGIN, target: TARGET }[],
18+
};
19+
20+
export function arrayDiff<ORIGIN, TARGET, HASH extends string | number>(originalArray: ORIGIN[], targetArray: TARGET[], originHash: (value: ORIGIN) => HASH, targetHash: (value: TARGET) => HASH): ArrayDiff<ORIGIN, TARGET> {
21+
const targetSet = new Set(targetArray.map(targetHash));
22+
23+
const elementsToAdd: TARGET[] = [];
24+
const elementsToRemove: ORIGIN[] = [];
25+
const elementsAlreadyPresent: { origin: ORIGIN, target: TARGET }[] = [];
26+
27+
targetArray.forEach(item => {
28+
const tHash = targetHash(item);
29+
const originElem = originalArray.find(orig => originHash(orig) === tHash);
30+
if (originElem === undefined) {
31+
elementsToAdd.push(item);
32+
} else {
33+
elementsAlreadyPresent.push({ target: item, origin: originElem });
34+
}
35+
});
36+
37+
originalArray.forEach(item => {
38+
if (!targetSet.has(originHash(item))) {
39+
elementsToRemove.push(item);
40+
}
41+
});
42+
43+
return {
44+
elementsToAdd,
45+
elementsToRemove,
46+
elementsAlreadyPresent,
47+
};
48+
}

0 commit comments

Comments
 (0)