north-star / commit
fixing problems
readme.md | 25 ++++++++-------- src/participants-sync.ts | 75 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 13 deletions(-) diff --git a/readme.md b/readme.md index acf2d02..1a3a948 100644 --- a/readme.md +++ b/readme.md @@ -49,27 +49,27 @@ Firebase console: [https://console.firebase.google.com/project/north-star-e5dee/ 1. Install dependencies: - ```bash - npm install - ``` + ```bash + npm install + ``` 2. Run the dev server: - ```bash - npm run dev - ``` + ```bash + npm run dev + ``` 3. Build production artifacts into `./public`: - ```bash - npm run build - ``` + ```bash + npm run build + ``` 4. Preview the production build locally: - ```bash - npm run preview - ``` + ```bash + npm run preview + ``` ## New Features @@ -103,7 +103,6 @@ You're looking at it. This project is licensed under the MIT License. - #### Why MIT for this project? Short version: emotional simplicity and momentum. diff --git a/src/participants-sync.ts b/src/participants-sync.ts new file mode 100644 index 0000000..7cdc7e6 --- /dev/null +++ b/src/participants-sync.ts @@ -0,0 +1,75 @@ +import { ref, onValue, type Database } from 'firebase/database'; +import { decryptJson } from './crypto'; + +type ParticipantRecord = { + id?: string; + encryptedState?: string; + encryptedPosition?: string; + [key: string]: unknown; +}; + +type ParticipantState = Record<string, unknown>; + +type SubscribeToParticipantsOptions = { + db: Database; + roomId: string; + roomKey: CryptoKey | null; + onUpdate: (participants: ParticipantState[]) => void; +}; + +/** + * Subscribes to the participants node for a room, decrypting each record + * as needed. Calls onUpdate(participants) after every snapshot resolves. + * + * @param {object} opts + * @param {object} opts.db - Firebase Database instance + * @param {string} opts.roomId + * @param {CryptoKey|null} opts.roomKey - AES-GCM key, or null for unencrypted rooms + * @param {function} opts.onUpdate - (participants: object[]) => void + * @returns {{ participants: object[] }} Shared array reference, mutated in place on each update + */ +export function subscribeToParticipants({ db, roomId, roomKey, onUpdate }: SubscribeToParticipantsOptions): { participants: ParticipantState[] } { + const participantsRef = ref(db, `rooms/${roomId}/participants`); + const participants: ParticipantState[] = []; + let snapshotVersion = 0; + + onValue(participantsRef, async (snapshot) => { + const version = ++snapshotVersion; + + const participantsData = snapshot.val(); + if (participantsData) { + const rawParticipants = Object.values(participantsData as Record<string, ParticipantRecord>); + const decrypted = await Promise.all(rawParticipants.map(async (p) => { + if (p.encryptedState && roomKey) { + try { + return await decryptJson(roomKey, p.encryptedState); + } catch (err) { + console.error('Failed to decrypt participant state', p.id, err); + return null; + } + } + if (p.encryptedPosition && roomKey) { + try { + const position = await decryptJson(roomKey, p.encryptedPosition); + const { encryptedPosition: _stripped, ...rest } = p; + return { ...rest, position }; + } catch (err) { + console.error('Failed to decrypt legacy participant position', p.id, err); + return null; + } + } + return p; + })); + if (version !== snapshotVersion) return; + participants.length = 0; + participants.push(...decrypted.filter((value): value is ParticipantState => Boolean(value))); + } else { + if (version !== snapshotVersion) return; + participants.length = 0; + } + + onUpdate(participants); + }); + + return { participants }; +}