@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.
- package/dual-write.js +21 -8
- package/package.json +1 -1
- 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
|
|
60
|
-
*
|
|
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
|
|
70
|
-
* save
|
|
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.
|
|
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
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
|
-
|
|
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');
|