@doubling/compound-sync 1.2.1 → 1.3.1
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 +43 -7
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
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* Shared by Me/ ← symlinks to files you've shared
|
|
12
12
|
* Shared with Me/ ← files shared with you (read-only)
|
|
13
13
|
*
|
|
14
|
-
* Usage: node sync.js [--config <file>] [--setup]
|
|
14
|
+
* Usage: node sync.js [--config <file>] [--setup] [--env <name>]
|
|
15
|
+
* node sync.js --version
|
|
16
|
+
* node sync.js --help
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
19
|
import { initializeApp } from 'firebase/app';
|
|
@@ -30,7 +32,7 @@ import os from 'os';
|
|
|
30
32
|
import http from 'http';
|
|
31
33
|
import { exec } from 'child_process';
|
|
32
34
|
import { fileURLToPath } from 'url';
|
|
33
|
-
import { pushFileToCloud, deleteFileFromCloud } from './dual-write.js';
|
|
35
|
+
import { pushFileToCloud, deleteFileFromCloud, readFileContent } from './dual-write.js';
|
|
34
36
|
|
|
35
37
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
36
38
|
|
|
@@ -420,6 +422,30 @@ async function initAppCheck(app, auth, tokenEndpoint) {
|
|
|
420
422
|
// --- Config ---
|
|
421
423
|
|
|
422
424
|
const args = process.argv.slice(2);
|
|
425
|
+
|
|
426
|
+
// Handle --version and --help early so they short-circuit before any
|
|
427
|
+
// Firebase init or browser-auth side effects.
|
|
428
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
429
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
|
|
430
|
+
console.log(pkg.version);
|
|
431
|
+
process.exit(0);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
435
|
+
console.log(`compound-sync - Doubling Compound sync daemon
|
|
436
|
+
|
|
437
|
+
Usage:
|
|
438
|
+
compound-sync Start the daemon (interactive setup on first run)
|
|
439
|
+
compound-sync --config <file> Use a specific config file
|
|
440
|
+
compound-sync --setup Force the setup wizard
|
|
441
|
+
compound-sync --env <name> Override env (sandbox|dev|prod)
|
|
442
|
+
compound-sync --version, -v Print version and exit
|
|
443
|
+
compound-sync --help, -h Print this help and exit
|
|
444
|
+
|
|
445
|
+
More: https://github.com/Doubling-Inc/doubling-compound`);
|
|
446
|
+
process.exit(0);
|
|
447
|
+
}
|
|
448
|
+
|
|
423
449
|
let configFile = 'config.json';
|
|
424
450
|
let forceSetup = false;
|
|
425
451
|
let envOverride = null;
|
|
@@ -615,7 +641,7 @@ ensureFolderStructure();
|
|
|
615
641
|
|
|
616
642
|
// --- Firestore → Local (multiple listeners) ---
|
|
617
643
|
|
|
618
|
-
function handleFirestoreChange(change, getLocalPath) {
|
|
644
|
+
async function handleFirestoreChange(change, getLocalPath) {
|
|
619
645
|
const data = change.doc.data();
|
|
620
646
|
const docPath = data.path;
|
|
621
647
|
if (!docPath) return;
|
|
@@ -648,7 +674,16 @@ function handleFirestoreChange(change, getLocalPath) {
|
|
|
648
674
|
console.log(` [firestore → local] ${action} ${docPath} (${data.scope || 'team'})`);
|
|
649
675
|
ensureDir(localPath);
|
|
650
676
|
suppressLocal.add(localPath);
|
|
651
|
-
|
|
677
|
+
|
|
678
|
+
// Read content from Cloud Storage. As of PR 6 there is no
|
|
679
|
+
// fallback; any read failure propagates to the caller's catch
|
|
680
|
+
// handler (handleFirestoreChange's promise rejection logger).
|
|
681
|
+
const content = await readFileContent({
|
|
682
|
+
storage,
|
|
683
|
+
orgId: ORG_ID,
|
|
684
|
+
fileId: change.doc.id,
|
|
685
|
+
});
|
|
686
|
+
fs.writeFileSync(localPath, content || '', 'utf-8');
|
|
652
687
|
lastKnownUpdate.set(trackingKey, remoteUpdated);
|
|
653
688
|
setTimeout(() => suppressLocal.delete(localPath), 1000);
|
|
654
689
|
}
|
|
@@ -663,7 +698,7 @@ function startFirestoreListeners() {
|
|
|
663
698
|
snapshot.docChanges().forEach(change => {
|
|
664
699
|
handleFirestoreChange(change, (data) => {
|
|
665
700
|
return path.join(LOCAL_PATH, teamFolder, data.path);
|
|
666
|
-
});
|
|
701
|
+
}).catch(err => console.error('Firestore change handler error:', err && err.message));
|
|
667
702
|
});
|
|
668
703
|
}, err => console.error(`Team "${teamFolder}" listener error:`, err));
|
|
669
704
|
console.log(` Listening: ${teamFolder}`);
|
|
@@ -676,7 +711,8 @@ function startFirestoreListeners() {
|
|
|
676
711
|
const data = change.doc.data();
|
|
677
712
|
const realPath = path.join(LOCAL_PATH, PRIVATE_FOLDER, data.path);
|
|
678
713
|
|
|
679
|
-
handleFirestoreChange(change, () => realPath)
|
|
714
|
+
handleFirestoreChange(change, () => realPath)
|
|
715
|
+
.catch(err => console.error('Firestore change handler error:', err && err.message));
|
|
680
716
|
|
|
681
717
|
if (change.type !== 'removed') {
|
|
682
718
|
updateSharedByMeSymlink(data, realPath);
|
|
@@ -695,7 +731,7 @@ function startFirestoreListeners() {
|
|
|
695
731
|
if (data.ownerId === USER_ID) return;
|
|
696
732
|
handleFirestoreChange(change, () => {
|
|
697
733
|
return path.join(LOCAL_PATH, SHARED_WITH_ME_FOLDER, data.path);
|
|
698
|
-
});
|
|
734
|
+
}).catch(err => console.error('Firestore change handler error:', err && err.message));
|
|
699
735
|
});
|
|
700
736
|
}, err => console.error('Shared with me listener error:', err));
|
|
701
737
|
console.log(' Listening: Shared with Me');
|