fuzzyworld

north-star / mobile / files / src / features / participants / sync.ts

Branch mobile · default branch · all branches

sync.ts

2.9 KB · history · blame

1import { ref, onValue, type Database } from 'firebase/database';
2import { decryptJson } from '../../core/crypto';
3
4type ParticipantRecord = {
5 id?: string;
6 encryptedState?: string;
7 encryptedPosition?: string;
8 [key: string]: unknown;
9};
10
11type ParticipantState = Record<string, unknown>;
12
13type SubscribeToParticipantsOptions = {
14 db: Database;
15 roomId: string;
16 roomKey: CryptoKey | null;
17 onUpdate: (participants: ParticipantState[]) => void;
18};
19
20/**
21 * Subscribes to the participants node for a room, decrypting each record
22 * as needed. Calls onUpdate(participants) after every snapshot resolves.
23 *
24 * @param {object} opts
25 * @param {object} opts.db - Firebase Database instance
26 * @param {string} opts.roomId
27 * @param {CryptoKey|null} opts.roomKey - AES-GCM key, or null for unencrypted rooms
28 * @param {function} opts.onUpdate - (participants: object[]) => void
29 * @returns {{ participants: object[] }} Shared array reference, mutated in place on each update
30 */
31export function subscribeToParticipants({ db, roomId, roomKey, onUpdate }: SubscribeToParticipantsOptions): { participants: ParticipantState[] } {
32 const participantsRef = ref(db, `rooms/${roomId}/participants`);
33 const participants: ParticipantState[] = [];
34 let snapshotVersion = 0;
35
36 onValue(participantsRef, async (snapshot) => {
37 const version = ++snapshotVersion;
38
39 const participantsData = snapshot.val();
40 if (participantsData) {
41 const rawParticipants = Object.values(participantsData as Record<string, ParticipantRecord>);
42 const decrypted = await Promise.all(rawParticipants.map(async (p) => {
43 if (p.encryptedState && roomKey) {
44 try {
45 return await decryptJson(roomKey, p.encryptedState);
46 } catch (err) {
47 console.error('Failed to decrypt participant state', p.id, err);
48 return null;
49 }
50 }
51 if (p.encryptedPosition && roomKey) {
52 try {
53 const position = await decryptJson(roomKey, p.encryptedPosition);
54 const { encryptedPosition: _stripped, ...rest } = p;
55 return { ...rest, position };
56 } catch (err) {
57 console.error('Failed to decrypt legacy participant position', p.id, err);
58 return null;
59 }
60 }
61 return p;
62 }));
63 if (version !== snapshotVersion) return;
64 participants.length = 0;
65 participants.push(...decrypted.filter((value): value is ParticipantState => Boolean(value)));
66 } else {
67 if (version !== snapshotVersion) return;
68 participants.length = 0;
69 }
70
71 onUpdate(participants);
72 });
73
74 return { participants };
75}