fuzzyworld

north-star / commit

fixing problems

a2e2426cdc19b075dcbd5e559f1ec928042c8a4b · Pavel Safronov · 2026-03-15 22:23:26 -0700 · parents 07aa39030c02

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 };
+}