pushwork 1.0.4 → 1.0.7
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/README.md +87 -328
- package/dist/.pushwork/automerge/3P/Dm3ekE2pmjGnWvDaG3vSR7ww98/snapshot/aa2349c94955ea561f698720142f9d884a6872d9f82dc332d578c216beb0df0e +0 -0
- package/dist/.pushwork/automerge/st/orage-adapter-id +1 -0
- package/dist/.pushwork/config.json +15 -0
- package/dist/.pushwork/snapshot.json +7 -0
- package/dist/cli.js +231 -170
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +51 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +799 -0
- package/dist/commands.js.map +1 -0
- package/dist/core/change-detection.d.ts +6 -19
- package/dist/core/change-detection.d.ts.map +1 -1
- package/dist/core/change-detection.js +101 -80
- package/dist/core/change-detection.js.map +1 -1
- package/dist/{config/index.d.ts → core/config.d.ts} +13 -3
- package/dist/core/config.d.ts.map +1 -0
- package/dist/{config/index.js → core/config.js} +55 -73
- package/dist/core/config.js.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/move-detection.d.ts +12 -50
- package/dist/core/move-detection.d.ts.map +1 -1
- package/dist/core/move-detection.js +58 -139
- package/dist/core/move-detection.js.map +1 -1
- package/dist/core/snapshot.d.ts +0 -4
- package/dist/core/snapshot.d.ts.map +1 -1
- package/dist/core/snapshot.js +2 -11
- package/dist/core/snapshot.js.map +1 -1
- package/dist/core/sync-engine.d.ts +5 -11
- package/dist/core/sync-engine.d.ts.map +1 -1
- package/dist/core/sync-engine.js +220 -362
- package/dist/core/sync-engine.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -6
- package/dist/index.js.map +1 -1
- package/dist/types/config.d.ts +43 -67
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +6 -0
- package/dist/types/config.js.map +1 -1
- package/dist/types/documents.d.ts +15 -3
- package/dist/types/documents.d.ts.map +1 -1
- package/dist/types/documents.js.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -3
- package/dist/types/index.js.map +1 -1
- package/dist/types/snapshot.d.ts +3 -21
- package/dist/types/snapshot.d.ts.map +1 -1
- package/dist/types/snapshot.js +0 -14
- package/dist/types/snapshot.js.map +1 -1
- package/dist/utils/content.d.ts.map +1 -1
- package/dist/utils/content.js +2 -6
- package/dist/utils/content.js.map +1 -1
- package/dist/utils/directory.d.ts +10 -0
- package/dist/utils/directory.d.ts.map +1 -0
- package/dist/utils/directory.js +37 -0
- package/dist/utils/directory.js.map +1 -0
- package/dist/utils/fs.d.ts +15 -2
- package/dist/utils/fs.d.ts.map +1 -1
- package/dist/utils/fs.js +63 -53
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -4
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/mime-types.d.ts.map +1 -1
- package/dist/utils/mime-types.js +11 -4
- package/dist/utils/mime-types.js.map +1 -1
- package/dist/utils/network-sync.d.ts +0 -6
- package/dist/utils/network-sync.d.ts.map +1 -1
- package/dist/utils/network-sync.js +55 -99
- package/dist/utils/network-sync.js.map +1 -1
- package/dist/utils/output.d.ts +129 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +375 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/utils/repo-factory.d.ts +2 -6
- package/dist/utils/repo-factory.d.ts.map +1 -1
- package/dist/utils/repo-factory.js +8 -22
- package/dist/utils/repo-factory.js.map +1 -1
- package/dist/utils/string-similarity.d.ts +14 -0
- package/dist/utils/string-similarity.d.ts.map +1 -0
- package/dist/utils/string-similarity.js +43 -0
- package/dist/utils/string-similarity.js.map +1 -0
- package/dist/utils/trace.d.ts +19 -0
- package/dist/utils/trace.d.ts.map +1 -0
- package/dist/utils/trace.js +68 -0
- package/dist/utils/trace.js.map +1 -0
- package/package.json +17 -12
- package/src/cli.ts +326 -252
- package/src/commands.ts +988 -0
- package/src/core/change-detection.ts +199 -162
- package/src/{config/index.ts → core/config.ts} +65 -82
- package/src/core/index.ts +1 -1
- package/src/core/move-detection.ts +74 -180
- package/src/core/snapshot.ts +2 -12
- package/src/core/sync-engine.ts +248 -499
- package/src/index.ts +0 -10
- package/src/types/config.ts +50 -72
- package/src/types/documents.ts +16 -3
- package/src/types/index.ts +0 -5
- package/src/types/snapshot.ts +1 -23
- package/src/utils/content.ts +2 -6
- package/src/utils/directory.ts +50 -0
- package/src/utils/fs.ts +67 -56
- package/src/utils/index.ts +1 -6
- package/src/utils/mime-types.ts +12 -4
- package/src/utils/network-sync.ts +79 -137
- package/src/utils/output.ts +450 -0
- package/src/utils/repo-factory.ts +13 -31
- package/src/utils/string-similarity.ts +54 -0
- package/src/utils/trace.ts +70 -0
- package/test/integration/exclude-patterns.test.ts +6 -15
- package/test/integration/fuzzer.test.ts +308 -391
- package/test/integration/init-sync.test.ts +89 -0
- package/test/integration/sync-deletion.test.ts +2 -61
- package/test/integration/sync-flow.test.ts +4 -24
- package/test/jest.setup.ts +34 -0
- package/test/unit/deletion-behavior.test.ts +3 -14
- package/test/unit/enhanced-mime-detection.test.ts +0 -22
- package/test/unit/snapshot.test.ts +2 -29
- package/test/unit/sync-convergence.test.ts +3 -198
- package/test/unit/sync-timing.test.ts +0 -44
- package/test/unit/utils.test.ts +0 -2
- package/tsconfig.json +3 -3
- package/dist/browser/browser-sync-engine.d.ts +0 -64
- package/dist/browser/browser-sync-engine.d.ts.map +0 -1
- package/dist/browser/browser-sync-engine.js +0 -303
- package/dist/browser/browser-sync-engine.js.map +0 -1
- package/dist/browser/filesystem-adapter.d.ts +0 -84
- package/dist/browser/filesystem-adapter.d.ts.map +0 -1
- package/dist/browser/filesystem-adapter.js +0 -413
- package/dist/browser/filesystem-adapter.js.map +0 -1
- package/dist/browser/index.d.ts +0 -36
- package/dist/browser/index.d.ts.map +0 -1
- package/dist/browser/index.js +0 -90
- package/dist/browser/index.js.map +0 -1
- package/dist/browser/types.d.ts +0 -70
- package/dist/browser/types.d.ts.map +0 -1
- package/dist/browser/types.js +0 -6
- package/dist/browser/types.js.map +0 -1
- package/dist/cli/commands.d.ts +0 -77
- package/dist/cli/commands.d.ts.map +0 -1
- package/dist/cli/commands.js +0 -904
- package/dist/cli/commands.js.map +0 -1
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -19
- package/dist/cli/index.js.map +0 -1
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/index.js.map +0 -1
- package/dist/core/isomorphic-snapshot.d.ts +0 -58
- package/dist/core/isomorphic-snapshot.d.ts.map +0 -1
- package/dist/core/isomorphic-snapshot.js +0 -204
- package/dist/core/isomorphic-snapshot.js.map +0 -1
- package/dist/platform/browser-filesystem.d.ts +0 -26
- package/dist/platform/browser-filesystem.d.ts.map +0 -1
- package/dist/platform/browser-filesystem.js +0 -91
- package/dist/platform/browser-filesystem.js.map +0 -1
- package/dist/platform/filesystem.d.ts +0 -29
- package/dist/platform/filesystem.d.ts.map +0 -1
- package/dist/platform/filesystem.js +0 -65
- package/dist/platform/filesystem.js.map +0 -1
- package/dist/platform/node-filesystem.d.ts +0 -21
- package/dist/platform/node-filesystem.d.ts.map +0 -1
- package/dist/platform/node-filesystem.js +0 -93
- package/dist/platform/node-filesystem.js.map +0 -1
- package/dist/utils/content-similarity.d.ts +0 -53
- package/dist/utils/content-similarity.d.ts.map +0 -1
- package/dist/utils/content-similarity.js +0 -155
- package/dist/utils/content-similarity.js.map +0 -1
- package/dist/utils/fs-browser.d.ts +0 -57
- package/dist/utils/fs-browser.d.ts.map +0 -1
- package/dist/utils/fs-browser.js +0 -311
- package/dist/utils/fs-browser.js.map +0 -1
- package/dist/utils/fs-node.d.ts +0 -53
- package/dist/utils/fs-node.d.ts.map +0 -1
- package/dist/utils/fs-node.js +0 -220
- package/dist/utils/fs-node.js.map +0 -1
- package/dist/utils/isomorphic.d.ts +0 -29
- package/dist/utils/isomorphic.d.ts.map +0 -1
- package/dist/utils/isomorphic.js +0 -139
- package/dist/utils/isomorphic.js.map +0 -1
- package/dist/utils/pure.d.ts +0 -25
- package/dist/utils/pure.d.ts.map +0 -1
- package/dist/utils/pure.js +0 -112
- package/dist/utils/pure.js.map +0 -1
- package/src/cli/commands.ts +0 -1207
- package/src/cli/index.ts +0 -2
- package/src/utils/content-similarity.ts +0 -194
- package/test/README-TESTING-GAPS.md +0 -174
- package/test/unit/content-similarity.test.ts +0 -236
package/dist/core/sync-engine.js
CHANGED
|
@@ -34,7 +34,6 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.SyncEngine = void 0;
|
|
37
|
-
const automerge_repo_1 = require("@automerge/automerge-repo");
|
|
38
37
|
const A = __importStar(require("@automerge/automerge"));
|
|
39
38
|
const types_1 = require("../types");
|
|
40
39
|
const utils_1 = require("../utils");
|
|
@@ -43,20 +42,27 @@ const network_sync_1 = require("../utils/network-sync");
|
|
|
43
42
|
const snapshot_1 = require("./snapshot");
|
|
44
43
|
const change_detection_1 = require("./change-detection");
|
|
45
44
|
const move_detection_1 = require("./move-detection");
|
|
45
|
+
const output_1 = require("../utils/output");
|
|
46
|
+
/**
|
|
47
|
+
* Post-sync delay constants for network propagation
|
|
48
|
+
* These delays allow the WebSocket protocol to propagate peer changes after
|
|
49
|
+
* our changes reach the server. waitForSync only ensures OUR changes reached
|
|
50
|
+
* the server, not that we've RECEIVED changes from other peers.
|
|
51
|
+
* TODO: remove need for this to exist.
|
|
52
|
+
*/
|
|
53
|
+
const POST_SYNC_DELAY_MS = 200; // After we pushed changes
|
|
46
54
|
/**
|
|
47
55
|
* Bidirectional sync engine implementing two-phase sync
|
|
48
56
|
*/
|
|
49
57
|
class SyncEngine {
|
|
50
|
-
constructor(repo, rootPath,
|
|
58
|
+
constructor(repo, rootPath, config) {
|
|
51
59
|
this.repo = repo;
|
|
52
60
|
this.rootPath = rootPath;
|
|
53
|
-
this.networkSyncEnabled = true;
|
|
54
61
|
this.handlesToWaitOn = [];
|
|
62
|
+
this.config = config;
|
|
55
63
|
this.snapshotManager = new snapshot_1.SnapshotManager(rootPath);
|
|
56
|
-
this.changeDetector = new change_detection_1.ChangeDetector(repo, rootPath,
|
|
57
|
-
this.moveDetector = new move_detection_1.MoveDetector();
|
|
58
|
-
this.networkSyncEnabled = networkSyncEnabled;
|
|
59
|
-
this.syncServerStorageId = syncServerStorageId;
|
|
64
|
+
this.changeDetector = new change_detection_1.ChangeDetector(repo, rootPath, config.exclude_patterns);
|
|
65
|
+
this.moveDetector = new move_detection_1.MoveDetector(config.sync.move_detection_threshold);
|
|
60
66
|
}
|
|
61
67
|
/**
|
|
62
68
|
* Determine if content should be treated as text for Automerge text operations
|
|
@@ -81,8 +87,7 @@ class SyncEngine {
|
|
|
81
87
|
/**
|
|
82
88
|
* Commit local changes only (no network sync)
|
|
83
89
|
*/
|
|
84
|
-
async commitLocal(
|
|
85
|
-
console.log(`🚀 Starting local commit process (dryRun: ${dryRun})`);
|
|
90
|
+
async commitLocal() {
|
|
86
91
|
const result = {
|
|
87
92
|
success: false,
|
|
88
93
|
filesChanged: 0,
|
|
@@ -92,35 +97,16 @@ class SyncEngine {
|
|
|
92
97
|
};
|
|
93
98
|
try {
|
|
94
99
|
// Load current snapshot
|
|
95
|
-
console.log(`📸 Loading current snapshot...`);
|
|
96
100
|
let snapshot = await this.snapshotManager.load();
|
|
97
101
|
if (!snapshot) {
|
|
98
|
-
console.log(`📸 No snapshot found, creating empty one`);
|
|
99
102
|
snapshot = this.snapshotManager.createEmpty();
|
|
100
103
|
}
|
|
101
|
-
else {
|
|
102
|
-
console.log(`📸 Snapshot loaded with ${snapshot.files.size} files`);
|
|
103
|
-
if (snapshot.rootDirectoryUrl) {
|
|
104
|
-
console.log(`🔗 Root directory URL: ${snapshot.rootDirectoryUrl}`);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// Backup snapshot before starting
|
|
108
|
-
if (!dryRun) {
|
|
109
|
-
console.log(`💾 Backing up snapshot...`);
|
|
110
|
-
await this.snapshotManager.backup();
|
|
111
|
-
}
|
|
112
104
|
// Detect all changes
|
|
113
|
-
console.log(`🔍 Detecting changes...`);
|
|
114
105
|
const changes = await this.changeDetector.detectChanges(snapshot);
|
|
115
|
-
console.log(`🔍 Found ${changes.length} changes`);
|
|
116
106
|
// Detect moves
|
|
117
|
-
|
|
118
|
-
const { moves, remainingChanges } = await this.moveDetector.detectMoves(changes, snapshot, this.rootPath);
|
|
119
|
-
console.log(`📦 Found ${moves.length} moves, ${remainingChanges.length} remaining changes`);
|
|
107
|
+
const { moves, remainingChanges } = await this.moveDetector.detectMoves(changes, snapshot);
|
|
120
108
|
// Apply local changes only (no network sync)
|
|
121
|
-
|
|
122
|
-
const commitResult = await this.pushLocalChanges(remainingChanges, moves, snapshot, dryRun);
|
|
123
|
-
console.log(`💾 Commit complete: ${commitResult.filesChanged} files changed`);
|
|
109
|
+
const commitResult = await this.pushLocalChanges(remainingChanges, moves, snapshot);
|
|
124
110
|
result.filesChanged += commitResult.filesChanged;
|
|
125
111
|
result.directoriesChanged += commitResult.directoriesChanged;
|
|
126
112
|
result.errors.push(...commitResult.errors);
|
|
@@ -128,18 +114,14 @@ class SyncEngine {
|
|
|
128
114
|
// Touch root directory if any changes were made
|
|
129
115
|
const hasChanges = result.filesChanged > 0 || result.directoriesChanged > 0;
|
|
130
116
|
if (hasChanges) {
|
|
131
|
-
await this.touchRootDirectory(snapshot
|
|
132
|
-
}
|
|
133
|
-
// Save updated snapshot if not dry run
|
|
134
|
-
if (!dryRun) {
|
|
135
|
-
await this.snapshotManager.save(snapshot);
|
|
117
|
+
await this.touchRootDirectory(snapshot);
|
|
136
118
|
}
|
|
119
|
+
// Save updated snapshot
|
|
120
|
+
await this.snapshotManager.save(snapshot);
|
|
137
121
|
result.success = result.errors.length === 0;
|
|
138
|
-
console.log(`💾 Local commit ${result.success ? "completed" : "failed"}`);
|
|
139
122
|
return result;
|
|
140
123
|
}
|
|
141
124
|
catch (error) {
|
|
142
|
-
console.error(`❌ Local commit failed: ${error}`);
|
|
143
125
|
result.errors.push({
|
|
144
126
|
path: this.rootPath,
|
|
145
127
|
operation: "commitLocal",
|
|
@@ -153,65 +135,43 @@ class SyncEngine {
|
|
|
153
135
|
/**
|
|
154
136
|
* Run full bidirectional sync
|
|
155
137
|
*/
|
|
156
|
-
async sync(
|
|
157
|
-
const syncStartTime = Date.now();
|
|
158
|
-
const timings = {};
|
|
138
|
+
async sync() {
|
|
159
139
|
const result = {
|
|
160
140
|
success: false,
|
|
161
141
|
filesChanged: 0,
|
|
162
142
|
directoriesChanged: 0,
|
|
163
143
|
errors: [],
|
|
164
144
|
warnings: [],
|
|
145
|
+
timings: {},
|
|
165
146
|
};
|
|
166
147
|
// Reset handles to wait on
|
|
167
148
|
this.handlesToWaitOn = [];
|
|
168
149
|
try {
|
|
169
150
|
// Load current snapshot
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
timings["load_snapshot"] = Date.now() - t0;
|
|
173
|
-
if (!snapshot) {
|
|
174
|
-
snapshot = this.snapshotManager.createEmpty();
|
|
175
|
-
}
|
|
176
|
-
// Backup snapshot before starting
|
|
177
|
-
const t1 = Date.now();
|
|
178
|
-
if (!dryRun) {
|
|
179
|
-
await this.snapshotManager.backup();
|
|
180
|
-
}
|
|
181
|
-
timings["backup_snapshot"] = Date.now() - t1;
|
|
151
|
+
const snapshot = (await this.snapshotManager.load()) ||
|
|
152
|
+
this.snapshotManager.createEmpty();
|
|
182
153
|
// Detect all changes
|
|
183
|
-
const t2 = Date.now();
|
|
184
154
|
const changes = await this.changeDetector.detectChanges(snapshot);
|
|
185
|
-
timings["detect_changes"] = Date.now() - t2;
|
|
186
155
|
// Detect moves
|
|
187
|
-
const
|
|
188
|
-
const { moves, remainingChanges } = await this.moveDetector.detectMoves(changes, snapshot, this.rootPath);
|
|
189
|
-
timings["detect_moves"] = Date.now() - t3;
|
|
190
|
-
if (changes.length > 0) {
|
|
191
|
-
console.log(`🔄 Syncing ${changes.length} changes...`);
|
|
192
|
-
}
|
|
156
|
+
const { moves, remainingChanges } = await this.moveDetector.detectMoves(changes, snapshot);
|
|
193
157
|
// Phase 1: Push local changes to remote
|
|
194
|
-
const
|
|
195
|
-
const phase1Result = await this.pushLocalChanges(remainingChanges, moves, snapshot, dryRun);
|
|
196
|
-
timings["phase1_push"] = Date.now() - t4;
|
|
158
|
+
const phase1Result = await this.pushLocalChanges(remainingChanges, moves, snapshot);
|
|
197
159
|
result.filesChanged += phase1Result.filesChanged;
|
|
198
160
|
result.directoriesChanged += phase1Result.directoriesChanged;
|
|
199
161
|
result.errors.push(...phase1Result.errors);
|
|
200
162
|
result.warnings.push(...phase1Result.warnings);
|
|
201
163
|
// Always wait for network sync when enabled (not just when local changes exist)
|
|
202
164
|
// This is critical for clone scenarios where we need to pull remote changes
|
|
203
|
-
|
|
204
|
-
if (!dryRun && this.networkSyncEnabled) {
|
|
165
|
+
if (this.config.sync_enabled) {
|
|
205
166
|
try {
|
|
206
167
|
// If we have a root directory URL, wait for it to sync
|
|
207
168
|
if (snapshot.rootDirectoryUrl) {
|
|
208
|
-
const
|
|
169
|
+
const rootDirUrl = snapshot.rootDirectoryUrl;
|
|
170
|
+
const rootHandle = await this.repo.find(rootDirUrl);
|
|
209
171
|
this.handlesToWaitOn.push(rootHandle);
|
|
210
172
|
}
|
|
211
173
|
if (this.handlesToWaitOn.length > 0) {
|
|
212
|
-
|
|
213
|
-
await (0, network_sync_1.waitForSync)(this.handlesToWaitOn, (0, network_sync_1.getSyncServerStorageId)(this.syncServerStorageId));
|
|
214
|
-
timings["network_sync"] = Date.now() - tWaitStart;
|
|
174
|
+
await (0, network_sync_1.waitForSync)(this.handlesToWaitOn, this.config.sync_server_storage_id);
|
|
215
175
|
// CRITICAL: Wait a bit after our changes reach the server to allow
|
|
216
176
|
// time for WebSocket to deliver OTHER peers' changes to us.
|
|
217
177
|
// waitForSync only ensures OUR changes reached the server, not that
|
|
@@ -221,30 +181,22 @@ class SyncEngine {
|
|
|
221
181
|
// each other due to timing races.
|
|
222
182
|
//
|
|
223
183
|
// Optimization: Only wait if we pushed changes (shorter delay if no changes)
|
|
224
|
-
|
|
225
|
-
const delayMs = phase1Result.filesChanged > 0 ? 200 : 100;
|
|
226
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
227
|
-
timings["post_sync_delay"] = Date.now() - tDelayStart;
|
|
184
|
+
await new Promise((resolve) => setTimeout(resolve, POST_SYNC_DELAY_MS));
|
|
228
185
|
}
|
|
229
186
|
}
|
|
230
187
|
catch (error) {
|
|
231
|
-
|
|
188
|
+
output_1.out.taskLine(`Network sync failed: ${error}`, true);
|
|
232
189
|
result.warnings.push(`Network sync failed: ${error}`);
|
|
233
190
|
}
|
|
234
191
|
}
|
|
235
|
-
timings["total_network"] = Date.now() - t5;
|
|
236
192
|
// Re-detect remote changes after network sync to ensure fresh state
|
|
237
193
|
// This fixes race conditions where we detect changes before server propagation
|
|
238
194
|
// NOTE: We DON'T update snapshot heads yet - that would prevent detecting remote changes!
|
|
239
|
-
const t6 = Date.now();
|
|
240
195
|
const freshChanges = await this.changeDetector.detectChanges(snapshot);
|
|
241
196
|
const freshRemoteChanges = freshChanges.filter((c) => c.changeType === types_1.ChangeType.REMOTE_ONLY ||
|
|
242
197
|
c.changeType === types_1.ChangeType.BOTH_CHANGED);
|
|
243
|
-
timings["redetect_changes"] = Date.now() - t6;
|
|
244
198
|
// Phase 2: Pull remote changes to local using fresh detection
|
|
245
|
-
const
|
|
246
|
-
const phase2Result = await this.pullRemoteChanges(freshRemoteChanges, snapshot, dryRun);
|
|
247
|
-
timings["phase2_pull"] = Date.now() - t7;
|
|
199
|
+
const phase2Result = await this.pullRemoteChanges(freshRemoteChanges, snapshot);
|
|
248
200
|
result.filesChanged += phase2Result.filesChanged;
|
|
249
201
|
result.directoriesChanged += phase2Result.directoriesChanged;
|
|
250
202
|
result.errors.push(...phase2Result.errors);
|
|
@@ -252,71 +204,47 @@ class SyncEngine {
|
|
|
252
204
|
// CRITICAL FIX: Update snapshot heads AFTER pulling remote changes
|
|
253
205
|
// This ensures that change detection can find remote changes, and we only
|
|
254
206
|
// update the snapshot after the filesystem is in sync with the documents
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
head: currentHeads,
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
catch (error) {
|
|
271
|
-
// Handle might not exist if file was deleted, skip
|
|
272
|
-
console.warn(`Could not update heads for ${filePath}: ${error}`);
|
|
207
|
+
// Update file document heads
|
|
208
|
+
for (const [filePath, snapshotEntry] of snapshot.files.entries()) {
|
|
209
|
+
try {
|
|
210
|
+
const handle = await this.repo.find(snapshotEntry.url);
|
|
211
|
+
const currentHeads = handle.heads();
|
|
212
|
+
if (!A.equals(currentHeads, snapshotEntry.head)) {
|
|
213
|
+
// Update snapshot with current heads after pulling changes
|
|
214
|
+
snapshot.files.set(filePath, {
|
|
215
|
+
...snapshotEntry,
|
|
216
|
+
head: currentHeads,
|
|
217
|
+
});
|
|
273
218
|
}
|
|
274
219
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
console.warn(`Could not update heads for directory ${dirPath}: ${error}`);
|
|
220
|
+
catch (error) {
|
|
221
|
+
// Handle might not exist if file was deleted
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Update directory document heads
|
|
225
|
+
for (const [dirPath, snapshotEntry] of snapshot.directories.entries()) {
|
|
226
|
+
try {
|
|
227
|
+
const handle = await this.repo.find(snapshotEntry.url);
|
|
228
|
+
const currentHeads = handle.heads();
|
|
229
|
+
if (!A.equals(currentHeads, snapshotEntry.head)) {
|
|
230
|
+
// Update snapshot with current heads after pulling changes
|
|
231
|
+
snapshot.directories.set(dirPath, {
|
|
232
|
+
...snapshotEntry,
|
|
233
|
+
head: currentHeads,
|
|
234
|
+
});
|
|
291
235
|
}
|
|
292
236
|
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
// Handle might not exist if directory was deleted
|
|
239
|
+
}
|
|
293
240
|
}
|
|
294
|
-
timings["update_snapshot_heads"] = Date.now() - t8;
|
|
295
241
|
// Touch root directory if any changes were made during sync
|
|
296
|
-
const t9 = Date.now();
|
|
297
242
|
const hasChanges = result.filesChanged > 0 || result.directoriesChanged > 0;
|
|
298
243
|
if (hasChanges) {
|
|
299
|
-
await this.touchRootDirectory(snapshot
|
|
244
|
+
await this.touchRootDirectory(snapshot);
|
|
300
245
|
}
|
|
301
|
-
timings["touch_root"] = Date.now() - t9;
|
|
302
246
|
// Save updated snapshot if not dry run
|
|
303
|
-
|
|
304
|
-
if (!dryRun) {
|
|
305
|
-
await this.snapshotManager.save(snapshot);
|
|
306
|
-
}
|
|
307
|
-
timings["save_snapshot"] = Date.now() - t10;
|
|
308
|
-
// Output timing breakdown if enabled via environment variable
|
|
309
|
-
if (process.env.PUSHWORK_TIMING === "1") {
|
|
310
|
-
const totalTime = Date.now() - syncStartTime;
|
|
311
|
-
console.error("\n⏱️ Sync Timing Breakdown:");
|
|
312
|
-
for (const [key, ms] of Object.entries(timings)) {
|
|
313
|
-
const pct = ((ms / totalTime) * 100).toFixed(1);
|
|
314
|
-
console.error(` ${key.padEnd(25)} ${ms.toString().padStart(5)}ms (${pct}%)`);
|
|
315
|
-
}
|
|
316
|
-
console.error(` ${"TOTAL".padEnd(25)} ${totalTime
|
|
317
|
-
.toString()
|
|
318
|
-
.padStart(5)}ms (100.0%)\n`);
|
|
319
|
-
}
|
|
247
|
+
await this.snapshotManager.save(snapshot);
|
|
320
248
|
result.success = result.errors.length === 0;
|
|
321
249
|
return result;
|
|
322
250
|
}
|
|
@@ -333,7 +261,7 @@ class SyncEngine {
|
|
|
333
261
|
/**
|
|
334
262
|
* Phase 1: Push local changes to Automerge documents
|
|
335
263
|
*/
|
|
336
|
-
async pushLocalChanges(changes, moves, snapshot
|
|
264
|
+
async pushLocalChanges(changes, moves, snapshot) {
|
|
337
265
|
const result = {
|
|
338
266
|
success: true,
|
|
339
267
|
filesChanged: 0,
|
|
@@ -341,26 +269,19 @@ class SyncEngine {
|
|
|
341
269
|
errors: [],
|
|
342
270
|
warnings: [],
|
|
343
271
|
};
|
|
344
|
-
// Process moves first
|
|
272
|
+
// Process moves first - all detected moves are applied
|
|
345
273
|
for (const move of moves) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
result.filesChanged++;
|
|
350
|
-
}
|
|
351
|
-
catch (error) {
|
|
352
|
-
result.errors.push({
|
|
353
|
-
path: move.fromPath,
|
|
354
|
-
operation: "move",
|
|
355
|
-
error: error,
|
|
356
|
-
recoverable: true,
|
|
357
|
-
});
|
|
358
|
-
}
|
|
274
|
+
try {
|
|
275
|
+
await this.applyMoveToRemote(move, snapshot);
|
|
276
|
+
result.filesChanged++;
|
|
359
277
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
278
|
+
catch (error) {
|
|
279
|
+
result.errors.push({
|
|
280
|
+
path: move.fromPath,
|
|
281
|
+
operation: "move",
|
|
282
|
+
error: error,
|
|
283
|
+
recoverable: true,
|
|
284
|
+
});
|
|
364
285
|
}
|
|
365
286
|
}
|
|
366
287
|
// Process local changes
|
|
@@ -368,7 +289,7 @@ class SyncEngine {
|
|
|
368
289
|
c.changeType === types_1.ChangeType.BOTH_CHANGED);
|
|
369
290
|
for (const change of localChanges) {
|
|
370
291
|
try {
|
|
371
|
-
await this.applyLocalChangeToRemote(change, snapshot
|
|
292
|
+
await this.applyLocalChangeToRemote(change, snapshot);
|
|
372
293
|
result.filesChanged++;
|
|
373
294
|
}
|
|
374
295
|
catch (error) {
|
|
@@ -385,7 +306,7 @@ class SyncEngine {
|
|
|
385
306
|
/**
|
|
386
307
|
* Phase 2: Pull remote changes to local filesystem
|
|
387
308
|
*/
|
|
388
|
-
async pullRemoteChanges(changes, snapshot
|
|
309
|
+
async pullRemoteChanges(changes, snapshot) {
|
|
389
310
|
const result = {
|
|
390
311
|
success: true,
|
|
391
312
|
filesChanged: 0,
|
|
@@ -400,7 +321,7 @@ class SyncEngine {
|
|
|
400
321
|
const sortedChanges = this.sortChangesByDependency(remoteChanges);
|
|
401
322
|
for (const change of sortedChanges) {
|
|
402
323
|
try {
|
|
403
|
-
await this.applyRemoteChangeToLocal(change, snapshot
|
|
324
|
+
await this.applyRemoteChangeToLocal(change, snapshot);
|
|
404
325
|
result.filesChanged++;
|
|
405
326
|
}
|
|
406
327
|
catch (error) {
|
|
@@ -417,33 +338,29 @@ class SyncEngine {
|
|
|
417
338
|
/**
|
|
418
339
|
* Apply local file change to remote Automerge document
|
|
419
340
|
*/
|
|
420
|
-
async applyLocalChangeToRemote(change, snapshot
|
|
341
|
+
async applyLocalChangeToRemote(change, snapshot) {
|
|
421
342
|
const snapshotEntry = snapshot.files.get(change.path);
|
|
422
343
|
// CRITICAL: Check for null explicitly, not falsy values
|
|
423
344
|
// Empty strings "" and empty Uint8Array are valid file content!
|
|
424
345
|
if (change.localContent === null) {
|
|
425
346
|
// File was deleted locally
|
|
426
347
|
if (snapshotEntry) {
|
|
427
|
-
|
|
428
|
-
await this.deleteRemoteFile(snapshotEntry.url, dryRun, snapshot, change.path);
|
|
348
|
+
await this.deleteRemoteFile(snapshotEntry.url, snapshot, change.path);
|
|
429
349
|
// Remove from directory document
|
|
430
|
-
await this.removeFileFromDirectory(snapshot, change.path
|
|
431
|
-
|
|
432
|
-
this.snapshotManager.removeFileEntry(snapshot, change.path);
|
|
433
|
-
}
|
|
350
|
+
await this.removeFileFromDirectory(snapshot, change.path);
|
|
351
|
+
this.snapshotManager.removeFileEntry(snapshot, change.path);
|
|
434
352
|
}
|
|
435
353
|
return;
|
|
436
354
|
}
|
|
437
355
|
if (!snapshotEntry) {
|
|
438
356
|
// New file
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
await this.addFileToDirectory(snapshot, change.path, handle.url, dryRun);
|
|
357
|
+
const handle = await this.createRemoteFile(change);
|
|
358
|
+
if (handle) {
|
|
359
|
+
await this.addFileToDirectory(snapshot, change.path, handle.url);
|
|
443
360
|
// CRITICAL FIX: Update snapshot with heads AFTER adding to directory
|
|
444
361
|
// The addFileToDirectory call above may have changed the document heads
|
|
445
362
|
this.snapshotManager.updateFileEntry(snapshot, change.path, {
|
|
446
|
-
path: (0, utils_1.
|
|
363
|
+
path: (0, utils_1.joinAndNormalizePath)(this.rootPath, change.path),
|
|
447
364
|
url: handle.url,
|
|
448
365
|
head: handle.heads(),
|
|
449
366
|
extension: (0, utils_1.getFileExtension)(change.path),
|
|
@@ -453,15 +370,14 @@ class SyncEngine {
|
|
|
453
370
|
}
|
|
454
371
|
else {
|
|
455
372
|
// Update existing file
|
|
456
|
-
|
|
457
|
-
await this.updateRemoteFile(snapshotEntry.url, change.localContent, dryRun, snapshot, change.path);
|
|
373
|
+
await this.updateRemoteFile(snapshotEntry.url, change.localContent, snapshot, change.path);
|
|
458
374
|
}
|
|
459
375
|
}
|
|
460
376
|
/**
|
|
461
377
|
* Apply remote change to local filesystem
|
|
462
378
|
*/
|
|
463
|
-
async applyRemoteChangeToLocal(change, snapshot
|
|
464
|
-
const localPath = (0, utils_1.
|
|
379
|
+
async applyRemoteChangeToLocal(change, snapshot) {
|
|
380
|
+
const localPath = (0, utils_1.joinAndNormalizePath)(this.rootPath, change.path);
|
|
465
381
|
if (!change.remoteHead) {
|
|
466
382
|
throw new Error(`No remote head found for remote change to ${change.path}`);
|
|
467
383
|
}
|
|
@@ -469,130 +385,114 @@ class SyncEngine {
|
|
|
469
385
|
// Empty strings "" and empty Uint8Array are valid file content!
|
|
470
386
|
if (change.remoteContent === null) {
|
|
471
387
|
// File was deleted remotely
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
await (0, utils_1.removePath)(localPath);
|
|
475
|
-
this.snapshotManager.removeFileEntry(snapshot, change.path);
|
|
476
|
-
}
|
|
388
|
+
await (0, utils_1.removePath)(localPath);
|
|
389
|
+
this.snapshotManager.removeFileEntry(snapshot, change.path);
|
|
477
390
|
return;
|
|
478
391
|
}
|
|
479
392
|
// Create or update local file
|
|
480
|
-
|
|
481
|
-
|
|
393
|
+
await (0, utils_1.writeFileContent)(localPath, change.remoteContent);
|
|
394
|
+
// Update or create snapshot entry for this file
|
|
395
|
+
const snapshotEntry = snapshot.files.get(change.path);
|
|
396
|
+
if (snapshotEntry) {
|
|
397
|
+
// Update existing entry
|
|
398
|
+
snapshotEntry.head = change.remoteHead;
|
|
482
399
|
}
|
|
483
400
|
else {
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (snapshot.rootDirectoryUrl) {
|
|
498
|
-
try {
|
|
499
|
-
const fileEntry = await this.findFileInDirectoryHierarchy(snapshot.rootDirectoryUrl, change.path);
|
|
500
|
-
if (fileEntry) {
|
|
501
|
-
this.snapshotManager.updateFileEntry(snapshot, change.path, {
|
|
502
|
-
path: localPath,
|
|
503
|
-
url: fileEntry.url,
|
|
504
|
-
head: change.remoteHead,
|
|
505
|
-
extension: (0, utils_1.getFileExtension)(change.path),
|
|
506
|
-
mimeType: (0, utils_1.getEnhancedMimeType)(change.path),
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
catch (error) {
|
|
511
|
-
console.warn(`Failed to update snapshot for remote file ${change.path}: ${error}`);
|
|
401
|
+
// Create new snapshot entry for newly discovered remote file
|
|
402
|
+
// We need to find the remote file's URL from the directory hierarchy
|
|
403
|
+
if (snapshot.rootDirectoryUrl) {
|
|
404
|
+
try {
|
|
405
|
+
const fileEntry = await (0, utils_1.findFileInDirectoryHierarchy)(this.repo, snapshot.rootDirectoryUrl, change.path);
|
|
406
|
+
if (fileEntry) {
|
|
407
|
+
this.snapshotManager.updateFileEntry(snapshot, change.path, {
|
|
408
|
+
path: localPath,
|
|
409
|
+
url: fileEntry.url,
|
|
410
|
+
head: change.remoteHead,
|
|
411
|
+
extension: (0, utils_1.getFileExtension)(change.path),
|
|
412
|
+
mimeType: (0, utils_1.getEnhancedMimeType)(change.path),
|
|
413
|
+
});
|
|
512
414
|
}
|
|
513
415
|
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
// Failed to update snapshot - file may have been deleted
|
|
418
|
+
output_1.out.taskLine(`Warning: Failed to update snapshot for remote file ${change.path}`, true);
|
|
419
|
+
}
|
|
514
420
|
}
|
|
515
421
|
}
|
|
516
422
|
}
|
|
517
423
|
/**
|
|
518
424
|
* Apply move to remote documents
|
|
519
425
|
*/
|
|
520
|
-
async applyMoveToRemote(move, snapshot
|
|
426
|
+
async applyMoveToRemote(move, snapshot) {
|
|
521
427
|
const fromEntry = snapshot.files.get(move.fromPath);
|
|
522
428
|
if (!fromEntry)
|
|
523
429
|
return;
|
|
524
430
|
// Parse paths
|
|
525
|
-
const fromParts = move.fromPath.split("/");
|
|
526
|
-
const fromFileName = fromParts.pop() || "";
|
|
527
|
-
const fromDirPath = fromParts.join("/");
|
|
528
431
|
const toParts = move.toPath.split("/");
|
|
529
432
|
const toFileName = toParts.pop() || "";
|
|
530
433
|
const toDirPath = toParts.join("/");
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (move.newContent
|
|
549
|
-
|
|
550
|
-
if (isText && typeof move.newContent === "string") {
|
|
551
|
-
(0, automerge_repo_1.updateText)(doc, ["content"], move.newContent);
|
|
552
|
-
}
|
|
553
|
-
else {
|
|
554
|
-
doc.content = move.newContent;
|
|
555
|
-
}
|
|
434
|
+
// 1) Remove file entry from old directory document
|
|
435
|
+
if (move.fromPath !== move.toPath) {
|
|
436
|
+
await this.removeFileFromDirectory(snapshot, move.fromPath);
|
|
437
|
+
}
|
|
438
|
+
// 2) Ensure destination directory document exists and add file entry there
|
|
439
|
+
await this.ensureDirectoryDocument(snapshot, toDirPath);
|
|
440
|
+
await this.addFileToDirectory(snapshot, move.toPath, fromEntry.url);
|
|
441
|
+
// 3) Update the FileDocument name and content to match new location/state
|
|
442
|
+
try {
|
|
443
|
+
const handle = await this.repo.find(fromEntry.url);
|
|
444
|
+
const heads = fromEntry.head;
|
|
445
|
+
// Update both name and content (if content changed during move)
|
|
446
|
+
if (heads && heads.length > 0) {
|
|
447
|
+
handle.changeAt(heads, (doc) => {
|
|
448
|
+
doc.name = toFileName;
|
|
449
|
+
// If new content is provided, update it (handles move + modification case)
|
|
450
|
+
if (move.newContent !== undefined) {
|
|
451
|
+
if (typeof move.newContent === "string") {
|
|
452
|
+
doc.content = new A.ImmutableString(move.newContent);
|
|
556
453
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
else {
|
|
560
|
-
handle.change((doc) => {
|
|
561
|
-
doc.name = toFileName;
|
|
562
|
-
// If new content is provided, update it (handles move + modification case)
|
|
563
|
-
if (move.newContent !== undefined) {
|
|
564
|
-
const isText = this.isTextContent(move.newContent);
|
|
565
|
-
if (isText && typeof move.newContent === "string") {
|
|
566
|
-
(0, automerge_repo_1.updateText)(doc, ["content"], move.newContent);
|
|
567
|
-
}
|
|
568
|
-
else {
|
|
569
|
-
doc.content = move.newContent;
|
|
570
|
-
}
|
|
454
|
+
else {
|
|
455
|
+
doc.content = move.newContent;
|
|
571
456
|
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
// Track file handle for network sync
|
|
575
|
-
this.handlesToWaitOn.push(handle);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
576
459
|
}
|
|
577
|
-
|
|
578
|
-
|
|
460
|
+
else {
|
|
461
|
+
handle.change((doc) => {
|
|
462
|
+
doc.name = toFileName;
|
|
463
|
+
// If new content is provided, update it (handles move + modification case)
|
|
464
|
+
if (move.newContent !== undefined) {
|
|
465
|
+
if (typeof move.newContent === "string") {
|
|
466
|
+
doc.content = new A.ImmutableString(move.newContent);
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
doc.content = move.newContent;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
});
|
|
579
473
|
}
|
|
580
|
-
//
|
|
581
|
-
this.
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
});
|
|
474
|
+
// Track file handle for network sync
|
|
475
|
+
this.handlesToWaitOn.push(handle);
|
|
476
|
+
}
|
|
477
|
+
catch (e) {
|
|
478
|
+
// Failed to update file name - file may have been deleted
|
|
479
|
+
output_1.out.taskLine(`Warning: Failed to rename ${move.fromPath} to ${move.toPath}`, true);
|
|
587
480
|
}
|
|
481
|
+
// 4) Update snapshot entries
|
|
482
|
+
this.snapshotManager.removeFileEntry(snapshot, move.fromPath);
|
|
483
|
+
this.snapshotManager.updateFileEntry(snapshot, move.toPath, {
|
|
484
|
+
...fromEntry,
|
|
485
|
+
path: (0, utils_1.joinAndNormalizePath)(this.rootPath, move.toPath),
|
|
486
|
+
head: fromEntry.head, // will be updated later when heads advance
|
|
487
|
+
});
|
|
588
488
|
}
|
|
589
489
|
/**
|
|
590
490
|
* Create new remote file document
|
|
591
491
|
*/
|
|
592
|
-
async createRemoteFile(change
|
|
492
|
+
async createRemoteFile(change) {
|
|
593
493
|
// CRITICAL: Check for null explicitly, not falsy values
|
|
594
494
|
// Empty strings "" and empty Uint8Array are valid file content!
|
|
595
|
-
if (
|
|
495
|
+
if (change.localContent === null)
|
|
596
496
|
return null;
|
|
597
497
|
const isText = this.isTextContent(change.localContent);
|
|
598
498
|
// Create initial document structure
|
|
@@ -601,16 +501,20 @@ class SyncEngine {
|
|
|
601
501
|
name: change.path.split("/").pop() || "",
|
|
602
502
|
extension: (0, utils_1.getFileExtension)(change.path),
|
|
603
503
|
mimeType: (0, utils_1.getEnhancedMimeType)(change.path),
|
|
604
|
-
content: isText
|
|
504
|
+
content: isText
|
|
505
|
+
? new A.ImmutableString("")
|
|
506
|
+
: typeof change.localContent === "string"
|
|
507
|
+
? new A.ImmutableString(change.localContent)
|
|
508
|
+
: change.localContent, // Empty ImmutableString for text, wrap strings for safety, actual content for binary
|
|
605
509
|
metadata: {
|
|
606
510
|
permissions: 0o644,
|
|
607
511
|
},
|
|
608
512
|
};
|
|
609
513
|
const handle = this.repo.create(fileDoc);
|
|
610
|
-
// For text files, use
|
|
514
|
+
// For text files, use ImmutableString for better performance
|
|
611
515
|
if (isText && typeof change.localContent === "string") {
|
|
612
516
|
handle.change((doc) => {
|
|
613
|
-
|
|
517
|
+
doc.content = new A.ImmutableString(change.localContent);
|
|
614
518
|
});
|
|
615
519
|
}
|
|
616
520
|
// Always track newly created files for network sync
|
|
@@ -621,9 +525,7 @@ class SyncEngine {
|
|
|
621
525
|
/**
|
|
622
526
|
* Update existing remote file document
|
|
623
527
|
*/
|
|
624
|
-
async updateRemoteFile(url, content,
|
|
625
|
-
if (dryRun)
|
|
626
|
-
return;
|
|
528
|
+
async updateRemoteFile(url, content, snapshot, filePath) {
|
|
627
529
|
const handle = await this.repo.find(url);
|
|
628
530
|
// Check if content actually changed before tracking for sync
|
|
629
531
|
const doc = await handle.doc();
|
|
@@ -642,7 +544,6 @@ class SyncEngine {
|
|
|
642
544
|
if (!contentChanged) {
|
|
643
545
|
// Content is identical, but we've updated the snapshot heads above
|
|
644
546
|
// This prevents fresh change detection from seeing stale heads
|
|
645
|
-
console.log(`🔍 Content is identical, but we've updated the snapshot heads above`);
|
|
646
547
|
return;
|
|
647
548
|
}
|
|
648
549
|
const heads = snapshotEntry?.head;
|
|
@@ -650,16 +551,15 @@ class SyncEngine {
|
|
|
650
551
|
throw new Error(`No heads found for ${url}`);
|
|
651
552
|
}
|
|
652
553
|
handle.changeAt(heads, (doc) => {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
(0, automerge_repo_1.updateText)(doc, ["content"], content);
|
|
554
|
+
if (typeof content === "string") {
|
|
555
|
+
doc.content = new A.ImmutableString(content);
|
|
656
556
|
}
|
|
657
557
|
else {
|
|
658
558
|
doc.content = content;
|
|
659
559
|
}
|
|
660
560
|
});
|
|
661
561
|
// Update snapshot with new heads after content change
|
|
662
|
-
if (
|
|
562
|
+
if (snapshotEntry) {
|
|
663
563
|
snapshot.files.set(filePath, {
|
|
664
564
|
...snapshotEntry,
|
|
665
565
|
head: handle.heads(),
|
|
@@ -671,9 +571,7 @@ class SyncEngine {
|
|
|
671
571
|
/**
|
|
672
572
|
* Delete remote file document
|
|
673
573
|
*/
|
|
674
|
-
async deleteRemoteFile(url,
|
|
675
|
-
if (dryRun)
|
|
676
|
-
return;
|
|
574
|
+
async deleteRemoteFile(url, snapshot, filePath) {
|
|
677
575
|
// In Automerge, we don't actually delete documents
|
|
678
576
|
// They become orphaned and will be garbage collected
|
|
679
577
|
// For now, we just mark them as deleted by clearing content
|
|
@@ -685,27 +583,26 @@ class SyncEngine {
|
|
|
685
583
|
}
|
|
686
584
|
if (heads) {
|
|
687
585
|
handle.changeAt(heads, (doc) => {
|
|
688
|
-
doc.content = "";
|
|
586
|
+
doc.content = new A.ImmutableString("");
|
|
689
587
|
});
|
|
690
588
|
}
|
|
691
589
|
else {
|
|
692
590
|
handle.change((doc) => {
|
|
693
|
-
doc.content = "";
|
|
591
|
+
doc.content = new A.ImmutableString("");
|
|
694
592
|
});
|
|
695
593
|
}
|
|
696
594
|
}
|
|
697
595
|
/**
|
|
698
596
|
* Add file entry to appropriate directory document (maintains hierarchy)
|
|
699
597
|
*/
|
|
700
|
-
async addFileToDirectory(snapshot, filePath, fileUrl
|
|
701
|
-
if (
|
|
598
|
+
async addFileToDirectory(snapshot, filePath, fileUrl) {
|
|
599
|
+
if (!snapshot.rootDirectoryUrl)
|
|
702
600
|
return;
|
|
703
601
|
const pathParts = filePath.split("/");
|
|
704
602
|
const fileName = pathParts.pop() || "";
|
|
705
603
|
const directoryPath = pathParts.join("/");
|
|
706
604
|
// Get or create the parent directory document
|
|
707
|
-
const parentDirUrl = await this.ensureDirectoryDocument(snapshot, directoryPath
|
|
708
|
-
console.log(`🔗 Adding ${fileName} (${fileUrl}) to directory ${parentDirUrl} (path: ${directoryPath})`);
|
|
605
|
+
const parentDirUrl = await this.ensureDirectoryDocument(snapshot, directoryPath);
|
|
709
606
|
const dirHandle = await this.repo.find(parentDirUrl);
|
|
710
607
|
let didChange = false;
|
|
711
608
|
const snapshotEntry = snapshot.directories.get(directoryPath);
|
|
@@ -749,7 +646,7 @@ class SyncEngine {
|
|
|
749
646
|
* Ensure directory document exists for the given path, creating hierarchy as needed
|
|
750
647
|
* First checks for existing shared directories before creating new ones
|
|
751
648
|
*/
|
|
752
|
-
async ensureDirectoryDocument(snapshot, directoryPath
|
|
649
|
+
async ensureDirectoryDocument(snapshot, directoryPath) {
|
|
753
650
|
// Root directory case
|
|
754
651
|
if (!directoryPath || directoryPath === "") {
|
|
755
652
|
return snapshot.rootDirectoryUrl;
|
|
@@ -764,7 +661,7 @@ class SyncEngine {
|
|
|
764
661
|
const currentDirName = pathParts.pop() || "";
|
|
765
662
|
const parentPath = pathParts.join("/");
|
|
766
663
|
// Ensure parent directory exists first (recursive)
|
|
767
|
-
const parentDirUrl = await this.ensureDirectoryDocument(snapshot, parentPath
|
|
664
|
+
const parentDirUrl = await this.ensureDirectoryDocument(snapshot, parentPath);
|
|
768
665
|
// DISCOVERY: Check if directory already exists in parent on server
|
|
769
666
|
try {
|
|
770
667
|
const parentHandle = await this.repo.find(parentDirUrl);
|
|
@@ -778,25 +675,22 @@ class SyncEngine {
|
|
|
778
675
|
const childDirHandle = await this.repo.find(existingDirEntry.url);
|
|
779
676
|
const childHeads = childDirHandle.heads();
|
|
780
677
|
// Update snapshot with discovered directory using validated heads
|
|
781
|
-
|
|
782
|
-
this.
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
});
|
|
788
|
-
}
|
|
678
|
+
this.snapshotManager.updateDirectoryEntry(snapshot, directoryPath, {
|
|
679
|
+
path: (0, utils_1.joinAndNormalizePath)(this.rootPath, directoryPath),
|
|
680
|
+
url: existingDirEntry.url,
|
|
681
|
+
head: childHeads,
|
|
682
|
+
entries: [],
|
|
683
|
+
});
|
|
789
684
|
return existingDirEntry.url;
|
|
790
685
|
}
|
|
791
686
|
catch (resolveErr) {
|
|
792
|
-
|
|
793
|
-
// Fall through to create a fresh directory document
|
|
687
|
+
// Failed to resolve directory - fall through to create a fresh directory document
|
|
794
688
|
}
|
|
795
689
|
}
|
|
796
690
|
}
|
|
797
691
|
}
|
|
798
692
|
catch (error) {
|
|
799
|
-
|
|
693
|
+
// Failed to check for existing directory - will create new one
|
|
800
694
|
}
|
|
801
695
|
// CREATE: Directory doesn't exist, create new one
|
|
802
696
|
const dirDoc = {
|
|
@@ -820,32 +714,30 @@ class SyncEngine {
|
|
|
820
714
|
}
|
|
821
715
|
});
|
|
822
716
|
// Track directory handles for sync
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
this.
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
});
|
|
841
|
-
}
|
|
717
|
+
this.handlesToWaitOn.push(dirHandle);
|
|
718
|
+
if (didChange) {
|
|
719
|
+
this.handlesToWaitOn.push(parentHandle);
|
|
720
|
+
// CRITICAL FIX: Update parent directory heads in snapshot immediately
|
|
721
|
+
// This prevents stale head issues when parent directory is modified
|
|
722
|
+
const parentSnapshotEntry = snapshot.directories.get(parentPath);
|
|
723
|
+
if (parentSnapshotEntry) {
|
|
724
|
+
parentSnapshotEntry.head = parentHandle.heads();
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
// Update snapshot with new directory
|
|
728
|
+
this.snapshotManager.updateDirectoryEntry(snapshot, directoryPath, {
|
|
729
|
+
path: (0, utils_1.joinAndNormalizePath)(this.rootPath, directoryPath),
|
|
730
|
+
url: dirHandle.url,
|
|
731
|
+
head: dirHandle.heads(),
|
|
732
|
+
entries: [],
|
|
733
|
+
});
|
|
842
734
|
return dirHandle.url;
|
|
843
735
|
}
|
|
844
736
|
/**
|
|
845
737
|
* Remove file entry from directory document
|
|
846
738
|
*/
|
|
847
|
-
async removeFileFromDirectory(snapshot, filePath
|
|
848
|
-
if (
|
|
739
|
+
async removeFileFromDirectory(snapshot, filePath) {
|
|
740
|
+
if (!snapshot.rootDirectoryUrl)
|
|
849
741
|
return;
|
|
850
742
|
const pathParts = filePath.split("/");
|
|
851
743
|
const fileName = pathParts.pop() || "";
|
|
@@ -858,7 +750,7 @@ class SyncEngine {
|
|
|
858
750
|
else {
|
|
859
751
|
const existingDir = snapshot.directories.get(directoryPath);
|
|
860
752
|
if (!existingDir) {
|
|
861
|
-
|
|
753
|
+
// Directory not found - file may already be removed
|
|
862
754
|
return;
|
|
863
755
|
}
|
|
864
756
|
parentDirUrl = existingDir.url;
|
|
@@ -876,7 +768,7 @@ class SyncEngine {
|
|
|
876
768
|
if (indexToRemove !== -1) {
|
|
877
769
|
doc.docs.splice(indexToRemove, 1);
|
|
878
770
|
didChange = true;
|
|
879
|
-
|
|
771
|
+
output_1.out.taskLine(`Removed ${fileName} from ${(0, utils_1.formatRelativePath)(directoryPath) || "root"}`);
|
|
880
772
|
}
|
|
881
773
|
});
|
|
882
774
|
}
|
|
@@ -886,7 +778,7 @@ class SyncEngine {
|
|
|
886
778
|
if (indexToRemove !== -1) {
|
|
887
779
|
doc.docs.splice(indexToRemove, 1);
|
|
888
780
|
didChange = true;
|
|
889
|
-
|
|
781
|
+
output_1.out.taskLine(`Removed ${fileName} from ${(0, utils_1.formatRelativePath)(directoryPath) || "root"}`);
|
|
890
782
|
}
|
|
891
783
|
});
|
|
892
784
|
}
|
|
@@ -897,43 +789,10 @@ class SyncEngine {
|
|
|
897
789
|
}
|
|
898
790
|
}
|
|
899
791
|
catch (error) {
|
|
900
|
-
|
|
792
|
+
// Failed to remove from directory - re-throw for caller to handle
|
|
901
793
|
throw error;
|
|
902
794
|
}
|
|
903
795
|
}
|
|
904
|
-
/**
|
|
905
|
-
* Find a file in the directory hierarchy by path
|
|
906
|
-
*/
|
|
907
|
-
async findFileInDirectoryHierarchy(directoryUrl, filePath) {
|
|
908
|
-
try {
|
|
909
|
-
const pathParts = filePath.split("/");
|
|
910
|
-
let currentDirUrl = directoryUrl;
|
|
911
|
-
// Navigate through directories to find the parent directory
|
|
912
|
-
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
913
|
-
const dirName = pathParts[i];
|
|
914
|
-
const dirHandle = await this.repo.find(currentDirUrl);
|
|
915
|
-
const dirDoc = await dirHandle.doc();
|
|
916
|
-
if (!dirDoc)
|
|
917
|
-
return null;
|
|
918
|
-
const subDirEntry = dirDoc.docs.find((entry) => entry.name === dirName && entry.type === "folder");
|
|
919
|
-
if (!subDirEntry)
|
|
920
|
-
return null;
|
|
921
|
-
currentDirUrl = subDirEntry.url;
|
|
922
|
-
}
|
|
923
|
-
// Now look for the file in the final directory
|
|
924
|
-
const fileName = pathParts[pathParts.length - 1];
|
|
925
|
-
const finalDirHandle = await this.repo.find(currentDirUrl);
|
|
926
|
-
const finalDirDoc = await finalDirHandle.doc();
|
|
927
|
-
if (!finalDirDoc)
|
|
928
|
-
return null;
|
|
929
|
-
const fileEntry = finalDirDoc.docs.find((entry) => entry.name === fileName && entry.type === "file");
|
|
930
|
-
return fileEntry || null;
|
|
931
|
-
}
|
|
932
|
-
catch (error) {
|
|
933
|
-
console.warn(`Failed to find file ${filePath} in directory hierarchy: ${error}`);
|
|
934
|
-
return null;
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
796
|
/**
|
|
938
797
|
* Sort changes by dependency order
|
|
939
798
|
*/
|
|
@@ -979,7 +838,7 @@ class SyncEngine {
|
|
|
979
838
|
};
|
|
980
839
|
}
|
|
981
840
|
const changes = await this.changeDetector.detectChanges(snapshot);
|
|
982
|
-
const { moves } = await this.moveDetector.detectMoves(changes, snapshot
|
|
841
|
+
const { moves } = await this.moveDetector.detectMoves(changes, snapshot);
|
|
983
842
|
const summary = this.generateChangeSummary(changes, moves);
|
|
984
843
|
return { changes, moves, summary };
|
|
985
844
|
}
|
|
@@ -1013,8 +872,8 @@ class SyncEngine {
|
|
|
1013
872
|
/**
|
|
1014
873
|
* Update the lastSyncAt timestamp on the root directory document
|
|
1015
874
|
*/
|
|
1016
|
-
async touchRootDirectory(snapshot
|
|
1017
|
-
if (
|
|
875
|
+
async touchRootDirectory(snapshot) {
|
|
876
|
+
if (!snapshot.rootDirectoryUrl) {
|
|
1018
877
|
return;
|
|
1019
878
|
}
|
|
1020
879
|
try {
|
|
@@ -1039,10 +898,9 @@ class SyncEngine {
|
|
|
1039
898
|
if (snapshotEntry) {
|
|
1040
899
|
snapshotEntry.head = rootHandle.heads();
|
|
1041
900
|
}
|
|
1042
|
-
console.log(`🕒 Updated root directory lastSyncAt to ${new Date(timestamp).toISOString()}`);
|
|
1043
901
|
}
|
|
1044
902
|
catch (error) {
|
|
1045
|
-
|
|
903
|
+
// Failed to update root directory timestamp
|
|
1046
904
|
}
|
|
1047
905
|
}
|
|
1048
906
|
}
|