@doubling/compound-sync 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dual-write.js +21 -8
  2. package/package.json +1 -1
  3. package/sync.js +16 -6
package/dual-write.js CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  doc, getDocs, setDoc, updateDoc, deleteDoc, query, where,
20
20
  } from 'firebase/firestore';
21
21
  import {
22
- ref as storageRef, uploadString, deleteObject,
22
+ ref as storageRef, uploadString, deleteObject, getBytes,
23
23
  } from 'firebase/storage';
24
24
 
25
25
  // ---- Pure helpers ----
@@ -53,11 +53,26 @@ export async function deleteBlob({ storage, orgId, fileId }) {
53
53
  }
54
54
  }
55
55
 
56
+ /**
57
+ * Read file content from Cloud Storage. Throws on any failure
58
+ * (missing blob, permission error, network). The Firestore content
59
+ * field is no longer written or read as of PR 6, so the read path
60
+ * has no fallback. Backfill (PR 4) is a prerequisite.
61
+ */
62
+ export async function readFileContent({ storage, orgId, fileId }) {
63
+ const blobPath = storagePathFor(orgId, fileId);
64
+ const bytes = await getBytes(storageRef(storage, blobPath));
65
+ return new TextDecoder('utf-8').decode(new Uint8Array(bytes));
66
+ }
67
+
56
68
  // ---- Core dual-write ----
57
69
 
58
70
  /**
59
- * Write a file's content to both the Firestore `content` field and a
60
- * Cloud Storage blob at `orgs/{orgId}/files/{fileId}/current.md`.
71
+ * Write a file's content to Cloud Storage at
72
+ * orgs/{orgId}/files/{fileId}/current.md and update the Firestore
73
+ * metadata doc with storagePath/contentHash/size. As of PR 6 the
74
+ * Firestore `content` field is no longer written; the blob is the
75
+ * sole source of file content.
61
76
  *
62
77
  * Returns one of:
63
78
  * { action: 'skipped', reason: 'shared-scope' }
@@ -66,8 +81,8 @@ export async function deleteBlob({ storage, orgId, fileId }) {
66
81
  * { action: 'updated', fileId, storageError? }
67
82
  *
68
83
  * `storageError` is populated when the Firestore write succeeded but
69
- * the Storage upload failed. Callers should log and continue; the next
70
- * save will re-upload because the hash on the metadata will not match.
84
+ * the Storage upload failed. Callers should log and retry; the next
85
+ * save attempts the upload again.
71
86
  */
72
87
  export async function pushFileToCloud({
73
88
  filesRef,
@@ -109,7 +124,6 @@ export async function pushFileToCloud({
109
124
  const fileId = newDocRef.id;
110
125
  const fileData = {
111
126
  path: docPath,
112
- content,
113
127
  createdAt: now,
114
128
  updatedAt: now,
115
129
  scope,
@@ -132,11 +146,10 @@ export async function pushFileToCloud({
132
146
 
133
147
  const docSnap = snapshot.docs[0];
134
148
  const existing = docSnap.data();
135
- if (existing.content === content && existing.contentHash === contentHash) {
149
+ if (existing.contentHash === contentHash) {
136
150
  return { action: 'noop', fileId: docSnap.id };
137
151
  }
138
152
  await updateDoc(docSnap.ref, {
139
- content,
140
153
  updatedAt: now,
141
154
  storagePath: storagePathFor(orgId, docSnap.id),
142
155
  contentHash,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doubling/compound-sync",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Bidirectional sync between Compound and local markdown files",
5
5
  "type": "module",
6
6
  "bin": {
package/sync.js CHANGED
@@ -30,7 +30,7 @@ import os from 'os';
30
30
  import http from 'http';
31
31
  import { exec } from 'child_process';
32
32
  import { fileURLToPath } from 'url';
33
- import { pushFileToCloud, deleteFileFromCloud } from './dual-write.js';
33
+ import { pushFileToCloud, deleteFileFromCloud, readFileContent } from './dual-write.js';
34
34
 
35
35
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
36
36
 
@@ -615,7 +615,7 @@ ensureFolderStructure();
615
615
 
616
616
  // --- Firestore → Local (multiple listeners) ---
617
617
 
618
- function handleFirestoreChange(change, getLocalPath) {
618
+ async function handleFirestoreChange(change, getLocalPath) {
619
619
  const data = change.doc.data();
620
620
  const docPath = data.path;
621
621
  if (!docPath) return;
@@ -648,7 +648,16 @@ function handleFirestoreChange(change, getLocalPath) {
648
648
  console.log(` [firestore → local] ${action} ${docPath} (${data.scope || 'team'})`);
649
649
  ensureDir(localPath);
650
650
  suppressLocal.add(localPath);
651
- fs.writeFileSync(localPath, data.content || '', 'utf-8');
651
+
652
+ // Read content from Cloud Storage. As of PR 6 there is no
653
+ // fallback; any read failure propagates to the caller's catch
654
+ // handler (handleFirestoreChange's promise rejection logger).
655
+ const content = await readFileContent({
656
+ storage,
657
+ orgId: ORG_ID,
658
+ fileId: change.doc.id,
659
+ });
660
+ fs.writeFileSync(localPath, content || '', 'utf-8');
652
661
  lastKnownUpdate.set(trackingKey, remoteUpdated);
653
662
  setTimeout(() => suppressLocal.delete(localPath), 1000);
654
663
  }
@@ -663,7 +672,7 @@ function startFirestoreListeners() {
663
672
  snapshot.docChanges().forEach(change => {
664
673
  handleFirestoreChange(change, (data) => {
665
674
  return path.join(LOCAL_PATH, teamFolder, data.path);
666
- });
675
+ }).catch(err => console.error('Firestore change handler error:', err && err.message));
667
676
  });
668
677
  }, err => console.error(`Team "${teamFolder}" listener error:`, err));
669
678
  console.log(` Listening: ${teamFolder}`);
@@ -676,7 +685,8 @@ function startFirestoreListeners() {
676
685
  const data = change.doc.data();
677
686
  const realPath = path.join(LOCAL_PATH, PRIVATE_FOLDER, data.path);
678
687
 
679
- handleFirestoreChange(change, () => realPath);
688
+ handleFirestoreChange(change, () => realPath)
689
+ .catch(err => console.error('Firestore change handler error:', err && err.message));
680
690
 
681
691
  if (change.type !== 'removed') {
682
692
  updateSharedByMeSymlink(data, realPath);
@@ -695,7 +705,7 @@ function startFirestoreListeners() {
695
705
  if (data.ownerId === USER_ID) return;
696
706
  handleFirestoreChange(change, () => {
697
707
  return path.join(LOCAL_PATH, SHARED_WITH_ME_FOLDER, data.path);
698
- });
708
+ }).catch(err => console.error('Firestore change handler error:', err && err.message));
699
709
  });
700
710
  }, err => console.error('Shared with me listener error:', err));
701
711
  console.log(' Listening: Shared with Me');