pushwork 1.1.4 → 2.0.0-a.sub.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/CLAUDE.md +9 -5
- package/dist/cli.js +48 -55
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +5 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +262 -263
- package/dist/commands.js.map +1 -1
- package/dist/core/change-detection.d.ts +1 -1
- package/dist/core/change-detection.d.ts.map +1 -1
- package/dist/core/change-detection.js +66 -103
- package/dist/core/change-detection.js.map +1 -1
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +14 -57
- package/dist/core/config.js.map +1 -1
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +5 -21
- package/dist/core/index.js.map +1 -1
- package/dist/core/move-detection.d.ts +2 -2
- package/dist/core/move-detection.d.ts.map +1 -1
- package/dist/core/move-detection.js +9 -13
- package/dist/core/move-detection.js.map +1 -1
- package/dist/core/snapshot.d.ts +1 -1
- package/dist/core/snapshot.d.ts.map +1 -1
- package/dist/core/snapshot.js +9 -46
- package/dist/core/snapshot.js.map +1 -1
- package/dist/core/sync-engine.d.ts +1 -1
- package/dist/core/sync-engine.d.ts.map +1 -1
- package/dist/core/sync-engine.js +113 -150
- package/dist/core/sync-engine.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -20
- package/dist/index.js.map +1 -1
- package/dist/types/config.d.ts +7 -6
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +1 -5
- package/dist/types/config.js.map +1 -1
- package/dist/types/documents.js +4 -7
- package/dist/types/documents.js.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +3 -19
- package/dist/types/index.js.map +1 -1
- package/dist/types/snapshot.js +1 -2
- package/dist/utils/content.js +4 -8
- package/dist/utils/content.js.map +1 -1
- package/dist/utils/directory.js +5 -9
- package/dist/utils/directory.js.map +1 -1
- package/dist/utils/fs.d.ts +1 -1
- package/dist/utils/fs.d.ts.map +1 -1
- package/dist/utils/fs.js +34 -84
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/index.d.ts +4 -4
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +4 -20
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/mime-types.js +5 -43
- package/dist/utils/mime-types.js.map +1 -1
- package/dist/utils/network-sync.d.ts +13 -8
- package/dist/utils/network-sync.d.ts.map +1 -1
- package/dist/utils/network-sync.js +65 -137
- package/dist/utils/network-sync.js.map +1 -1
- package/dist/utils/node-polyfills.d.ts +9 -0
- package/dist/utils/node-polyfills.d.ts.map +1 -0
- package/dist/utils/node-polyfills.js +9 -0
- package/dist/utils/node-polyfills.js.map +1 -0
- package/dist/utils/output.js +32 -39
- package/dist/utils/output.js.map +1 -1
- package/dist/utils/repo-factory.d.ts +8 -2
- package/dist/utils/repo-factory.d.ts.map +1 -1
- package/dist/utils/repo-factory.js +38 -47
- package/dist/utils/repo-factory.js.map +1 -1
- package/dist/utils/string-similarity.js +1 -5
- package/dist/utils/string-similarity.js.map +1 -1
- package/dist/utils/text-diff.js +5 -43
- package/dist/utils/text-diff.js.map +1 -1
- package/dist/utils/trace.js +6 -11
- package/dist/utils/trace.js.map +1 -1
- package/package.json +7 -5
- package/src/cli.ts +25 -34
- package/src/commands.ts +75 -11
- package/src/core/change-detection.ts +4 -4
- package/src/core/config.ts +2 -12
- package/src/core/index.ts +5 -5
- package/src/core/move-detection.ts +4 -4
- package/src/core/snapshot.ts +3 -3
- package/src/core/sync-engine.ts +11 -16
- package/src/index.ts +4 -4
- package/src/types/config.ts +8 -8
- package/src/types/index.ts +3 -3
- package/src/utils/directory.ts +1 -1
- package/src/utils/fs.ts +6 -4
- package/src/utils/index.ts +4 -4
- package/src/utils/network-sync.ts +62 -115
- package/src/utils/node-polyfills.ts +8 -0
- package/src/utils/repo-factory.ts +55 -10
- package/src/utils/trace.ts +1 -1
- package/tsconfig.json +2 -1
package/dist/core/sync-engine.js
CHANGED
|
@@ -1,50 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.SyncEngine = void 0;
|
|
37
|
-
const automerge_repo_1 = require("@automerge/automerge-repo");
|
|
38
|
-
const A = __importStar(require("@automerge/automerge"));
|
|
39
|
-
const types_1 = require("../types");
|
|
40
|
-
const utils_1 = require("../utils");
|
|
41
|
-
const content_1 = require("../utils/content");
|
|
42
|
-
const network_sync_1 = require("../utils/network-sync");
|
|
43
|
-
const snapshot_1 = require("./snapshot");
|
|
44
|
-
const change_detection_1 = require("./change-detection");
|
|
45
|
-
const move_detection_1 = require("./move-detection");
|
|
46
|
-
const output_1 = require("../utils/output");
|
|
47
|
-
const path = __importStar(require("path"));
|
|
1
|
+
import { parseAutomergeUrl, stringifyAutomergeUrl, } from "@automerge/automerge-repo";
|
|
2
|
+
import * as A from "@automerge/automerge";
|
|
3
|
+
import { ChangeType, FileType, } from "../types/index.js";
|
|
4
|
+
import { writeFileContent, removePath, getFileExtension, getEnhancedMimeType, formatRelativePath, findFileInDirectoryHierarchy, joinAndNormalizePath, getPlainUrl, updateTextContent, readDocContent, } from "../utils/index.js";
|
|
5
|
+
import { isContentEqual, contentHash } from "../utils/content.js";
|
|
6
|
+
import { waitForSync, waitForBidirectionalSync } from "../utils/network-sync.js";
|
|
7
|
+
import { SnapshotManager } from "./snapshot.js";
|
|
8
|
+
import { ChangeDetector } from "./change-detection.js";
|
|
9
|
+
import { MoveDetector } from "./move-detection.js";
|
|
10
|
+
import { out } from "../utils/output.js";
|
|
11
|
+
import * as path from "path";
|
|
48
12
|
const isDebug = !!process.env.DEBUG;
|
|
49
13
|
function debug(...args) {
|
|
50
14
|
if (isDebug)
|
|
@@ -69,7 +33,7 @@ const BIDIRECTIONAL_SYNC_TIMEOUT_MS = 5000; // Timeout for bidirectional sync st
|
|
|
69
33
|
/**
|
|
70
34
|
* Bidirectional sync engine implementing two-phase sync
|
|
71
35
|
*/
|
|
72
|
-
class SyncEngine {
|
|
36
|
+
export class SyncEngine {
|
|
73
37
|
constructor(repo, rootPath, config) {
|
|
74
38
|
this.repo = repo;
|
|
75
39
|
this.rootPath = rootPath;
|
|
@@ -77,9 +41,9 @@ class SyncEngine {
|
|
|
77
41
|
// Path depth determines sync order (deepest first)
|
|
78
42
|
this.handlesByPath = new Map();
|
|
79
43
|
this.config = config;
|
|
80
|
-
this.snapshotManager = new
|
|
81
|
-
this.changeDetector = new
|
|
82
|
-
this.moveDetector = new
|
|
44
|
+
this.snapshotManager = new SnapshotManager(rootPath);
|
|
45
|
+
this.changeDetector = new ChangeDetector(repo, rootPath, config.exclude_patterns, config.artifact_directories || []);
|
|
46
|
+
this.moveDetector = new MoveDetector(config.sync.move_detection_threshold);
|
|
83
47
|
}
|
|
84
48
|
/**
|
|
85
49
|
* Determine if content should be treated as text for Automerge text operations
|
|
@@ -95,9 +59,9 @@ class SyncEngine {
|
|
|
95
59
|
* This ensures clients can fetch the exact version of the document.
|
|
96
60
|
*/
|
|
97
61
|
getVersionedUrl(handle) {
|
|
98
|
-
const { documentId } =
|
|
62
|
+
const { documentId } = parseAutomergeUrl(handle.url);
|
|
99
63
|
const heads = handle.heads();
|
|
100
|
-
return
|
|
64
|
+
return stringifyAutomergeUrl({ documentId, heads });
|
|
101
65
|
}
|
|
102
66
|
/**
|
|
103
67
|
* Determine if a file path is inside an artifact directory.
|
|
@@ -117,7 +81,7 @@ class SyncEngine {
|
|
|
117
81
|
if (this.isArtifactPath(filePath)) {
|
|
118
82
|
return this.getVersionedUrl(handle);
|
|
119
83
|
}
|
|
120
|
-
return
|
|
84
|
+
return getPlainUrl(handle.url);
|
|
121
85
|
}
|
|
122
86
|
/**
|
|
123
87
|
* Set the root directory URL in the snapshot
|
|
@@ -153,7 +117,7 @@ class SyncEngine {
|
|
|
153
117
|
return;
|
|
154
118
|
// Clear the root directory document's entries
|
|
155
119
|
if (snapshot.rootDirectoryUrl) {
|
|
156
|
-
const rootHandle = await this.repo.find(
|
|
120
|
+
const rootHandle = await this.repo.find(getPlainUrl(snapshot.rootDirectoryUrl));
|
|
157
121
|
rootHandle.change((doc) => {
|
|
158
122
|
doc.docs.splice(0, doc.docs.length);
|
|
159
123
|
});
|
|
@@ -216,15 +180,15 @@ class SyncEngine {
|
|
|
216
180
|
* Returns new handles that should be retried for sync.
|
|
217
181
|
*/
|
|
218
182
|
async recreateFailedDocuments(failedHandles, snapshot) {
|
|
219
|
-
const failedUrls = new Set(failedHandles.map(h =>
|
|
183
|
+
const failedUrls = new Set(failedHandles.map(h => getPlainUrl(h.url)));
|
|
220
184
|
const newHandles = [];
|
|
221
185
|
// Find which paths correspond to the failed handles
|
|
222
186
|
for (const [filePath, entry] of snapshot.files.entries()) {
|
|
223
|
-
const plainUrl =
|
|
187
|
+
const plainUrl = getPlainUrl(entry.url);
|
|
224
188
|
if (!failedUrls.has(plainUrl))
|
|
225
189
|
continue;
|
|
226
190
|
debug(`recreate: recreating document for ${filePath} (${plainUrl})`);
|
|
227
|
-
|
|
191
|
+
out.taskLine(`Recreating document for ${filePath}`);
|
|
228
192
|
try {
|
|
229
193
|
// Read the current content from the old handle
|
|
230
194
|
const oldHandle = await this.repo.find(plainUrl);
|
|
@@ -233,7 +197,7 @@ class SyncEngine {
|
|
|
233
197
|
debug(`recreate: could not read doc for ${filePath}, skipping`);
|
|
234
198
|
continue;
|
|
235
199
|
}
|
|
236
|
-
const content =
|
|
200
|
+
const content = readDocContent(doc.content);
|
|
237
201
|
if (content === null) {
|
|
238
202
|
debug(`recreate: null content for ${filePath}, skipping`);
|
|
239
203
|
continue;
|
|
@@ -241,8 +205,8 @@ class SyncEngine {
|
|
|
241
205
|
// Create a fresh document
|
|
242
206
|
const fakeChange = {
|
|
243
207
|
path: filePath,
|
|
244
|
-
changeType:
|
|
245
|
-
fileType: this.isTextContent(content) ?
|
|
208
|
+
changeType: ChangeType.LOCAL_ONLY,
|
|
209
|
+
fileType: this.isTextContent(content) ? FileType.TEXT : FileType.BINARY,
|
|
246
210
|
localContent: content,
|
|
247
211
|
remoteContent: null,
|
|
248
212
|
};
|
|
@@ -255,7 +219,7 @@ class SyncEngine {
|
|
|
255
219
|
...entry,
|
|
256
220
|
url: entryUrl,
|
|
257
221
|
head: newHandle.heads(),
|
|
258
|
-
...(this.isArtifactPath(filePath) ? { contentHash:
|
|
222
|
+
...(this.isArtifactPath(filePath) ? { contentHash: contentHash(content) } : {}),
|
|
259
223
|
});
|
|
260
224
|
// Update parent directory entry to point to new document
|
|
261
225
|
const pathParts = filePath.split("/");
|
|
@@ -271,7 +235,7 @@ class SyncEngine {
|
|
|
271
235
|
continue;
|
|
272
236
|
dirUrl = dirEntry.url;
|
|
273
237
|
}
|
|
274
|
-
const dirHandle = await this.repo.find(
|
|
238
|
+
const dirHandle = await this.repo.find(getPlainUrl(dirUrl));
|
|
275
239
|
dirHandle.change((d) => {
|
|
276
240
|
const idx = d.docs.findIndex(e => e.name === fileName && e.type === "file");
|
|
277
241
|
if (idx !== -1) {
|
|
@@ -287,18 +251,18 @@ class SyncEngine {
|
|
|
287
251
|
}
|
|
288
252
|
catch (error) {
|
|
289
253
|
debug(`recreate: failed for ${filePath}: ${error}`);
|
|
290
|
-
|
|
254
|
+
out.taskLine(`Failed to recreate ${filePath}: ${error}`, true);
|
|
291
255
|
}
|
|
292
256
|
}
|
|
293
257
|
// Also check directory documents
|
|
294
258
|
for (const [dirPath, entry] of snapshot.directories.entries()) {
|
|
295
|
-
const plainUrl =
|
|
259
|
+
const plainUrl = getPlainUrl(entry.url);
|
|
296
260
|
if (!failedUrls.has(plainUrl))
|
|
297
261
|
continue;
|
|
298
262
|
// Directory docs can't be easily recreated (they reference children).
|
|
299
263
|
// Just log a warning — the child recreation above should handle most cases.
|
|
300
264
|
debug(`recreate: directory ${dirPath || "(root)"} failed to sync, cannot recreate`);
|
|
301
|
-
|
|
265
|
+
out.taskLine(`Warning: directory ${dirPath || "(root)"} failed to sync`, true);
|
|
302
266
|
}
|
|
303
267
|
return newHandles;
|
|
304
268
|
}
|
|
@@ -324,12 +288,12 @@ class SyncEngine {
|
|
|
324
288
|
// Wait for initial sync to receive any pending remote changes
|
|
325
289
|
if (this.config.sync_enabled && snapshot.rootDirectoryUrl) {
|
|
326
290
|
debug("sync: waiting for root document to be ready");
|
|
327
|
-
|
|
291
|
+
out.update("Waiting for root document from server");
|
|
328
292
|
// Wait for the root document to be fetched from the network.
|
|
329
293
|
// repo.find() rejects with "unavailable" if the server doesn't
|
|
330
294
|
// have the document yet, so we retry with backoff.
|
|
331
295
|
// This is critical for clone scenarios.
|
|
332
|
-
const plainRootUrl =
|
|
296
|
+
const plainRootUrl = getPlainUrl(snapshot.rootDirectoryUrl);
|
|
333
297
|
const maxAttempts = 6;
|
|
334
298
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
335
299
|
try {
|
|
@@ -343,32 +307,32 @@ class SyncEngine {
|
|
|
343
307
|
if (isUnavailable && attempt < maxAttempts) {
|
|
344
308
|
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
|
|
345
309
|
debug(`sync: root document not available (attempt ${attempt}/${maxAttempts}), retrying in ${delay}ms`);
|
|
346
|
-
|
|
310
|
+
out.update(`Waiting for root document (attempt ${attempt}/${maxAttempts})`);
|
|
347
311
|
await new Promise(r => setTimeout(r, delay));
|
|
348
312
|
}
|
|
349
313
|
else {
|
|
350
314
|
debug(`sync: root document unavailable after ${maxAttempts} attempts: ${error}`);
|
|
351
|
-
|
|
315
|
+
out.taskLine(`Root document unavailable: ${error}`, true);
|
|
352
316
|
break;
|
|
353
317
|
}
|
|
354
318
|
}
|
|
355
319
|
}
|
|
356
320
|
debug("sync: waiting for initial bidirectional sync");
|
|
357
|
-
|
|
321
|
+
out.update("Waiting for initial sync from server");
|
|
358
322
|
try {
|
|
359
|
-
await
|
|
323
|
+
await waitForBidirectionalSync(this.repo, snapshot.rootDirectoryUrl, {
|
|
360
324
|
timeoutMs: 5000, // Increased timeout for initial sync
|
|
361
325
|
pollIntervalMs: 100,
|
|
362
326
|
stableChecksRequired: 3,
|
|
363
327
|
});
|
|
364
328
|
}
|
|
365
329
|
catch (error) {
|
|
366
|
-
|
|
330
|
+
out.taskLine(`Initial sync: ${error}`, true);
|
|
367
331
|
}
|
|
368
332
|
}
|
|
369
333
|
// Detect all changes
|
|
370
334
|
debug("sync: detecting changes");
|
|
371
|
-
|
|
335
|
+
out.update("Detecting local and remote changes");
|
|
372
336
|
// Capture pre-push snapshot file paths to detect deletions after push
|
|
373
337
|
const prePushFilePaths = new Set(snapshot.files.keys());
|
|
374
338
|
const changes = await this.changeDetector.detectChanges(snapshot);
|
|
@@ -396,17 +360,17 @@ class SyncEngine {
|
|
|
396
360
|
const allHandles = Array.from(this.handlesByPath.values());
|
|
397
361
|
const handlePaths = Array.from(this.handlesByPath.keys());
|
|
398
362
|
debug(`sync: waiting for ${allHandles.length} handles to sync to server: ${handlePaths.slice(0, 10).map(p => p || "(root)").join(", ")}${handlePaths.length > 10 ? ` ...and ${handlePaths.length - 10} more` : ""}`);
|
|
399
|
-
|
|
400
|
-
const { failed } = await
|
|
363
|
+
out.update(`Uploading ${allHandles.length} documents to sync server`);
|
|
364
|
+
const { failed } = await waitForSync(allHandles);
|
|
401
365
|
// Recreate failed documents and retry once
|
|
402
366
|
if (failed.length > 0) {
|
|
403
367
|
debug(`sync: ${failed.length} documents failed, recreating`);
|
|
404
|
-
|
|
368
|
+
out.update(`Recreating ${failed.length} failed documents`);
|
|
405
369
|
const retryHandles = await this.recreateFailedDocuments(failed, snapshot);
|
|
406
370
|
if (retryHandles.length > 0) {
|
|
407
371
|
debug(`sync: retrying ${retryHandles.length} recreated handles`);
|
|
408
|
-
|
|
409
|
-
const retry = await
|
|
372
|
+
out.update(`Retrying ${retryHandles.length} recreated documents`);
|
|
373
|
+
const retry = await waitForSync(retryHandles);
|
|
410
374
|
if (retry.failed.length > 0) {
|
|
411
375
|
const msg = `${retry.failed.length} documents failed to sync to server after recreation`;
|
|
412
376
|
debug(`sync: ${msg}`);
|
|
@@ -425,8 +389,8 @@ class SyncEngine {
|
|
|
425
389
|
// Use tracked handles for post-push check (cheaper than full tree scan)
|
|
426
390
|
const changedHandles = Array.from(this.handlesByPath.values());
|
|
427
391
|
debug(`sync: waiting for bidirectional sync to stabilize (${changedHandles.length} tracked handles)`);
|
|
428
|
-
|
|
429
|
-
await
|
|
392
|
+
out.update("Waiting for bidirectional sync to stabilize");
|
|
393
|
+
await waitForBidirectionalSync(this.repo, snapshot.rootDirectoryUrl, {
|
|
430
394
|
timeoutMs: BIDIRECTIONAL_SYNC_TIMEOUT_MS,
|
|
431
395
|
pollIntervalMs: 100,
|
|
432
396
|
stableChecksRequired: 3,
|
|
@@ -441,13 +405,13 @@ class SyncEngine {
|
|
|
441
405
|
await this.touchRootDirectory(snapshot);
|
|
442
406
|
const rootHandle = await this.repo.find(snapshot.rootDirectoryUrl);
|
|
443
407
|
debug("sync: syncing root directory touch to server");
|
|
444
|
-
|
|
445
|
-
await
|
|
408
|
+
out.update("Syncing root directory update");
|
|
409
|
+
await waitForSync([rootHandle]);
|
|
446
410
|
}
|
|
447
411
|
}
|
|
448
412
|
catch (error) {
|
|
449
413
|
debug(`sync: network sync error: ${error}`);
|
|
450
|
-
|
|
414
|
+
out.taskLine(`Network sync failed: ${error}`, true);
|
|
451
415
|
result.errors.push({
|
|
452
416
|
path: "sync",
|
|
453
417
|
operation: "network-sync",
|
|
@@ -469,11 +433,11 @@ class SyncEngine {
|
|
|
469
433
|
}
|
|
470
434
|
debug("sync: re-detecting changes after network sync");
|
|
471
435
|
const freshChanges = await this.changeDetector.detectChanges(snapshot, deletedPaths);
|
|
472
|
-
const freshRemoteChanges = freshChanges.filter(c => c.changeType ===
|
|
473
|
-
c.changeType ===
|
|
436
|
+
const freshRemoteChanges = freshChanges.filter(c => c.changeType === ChangeType.REMOTE_ONLY ||
|
|
437
|
+
c.changeType === ChangeType.BOTH_CHANGED);
|
|
474
438
|
debug(`sync: phase 2 - pulling ${freshRemoteChanges.length} remote changes`);
|
|
475
439
|
if (freshRemoteChanges.length > 0) {
|
|
476
|
-
|
|
440
|
+
out.update(`Pulling ${freshRemoteChanges.length} remote changes`);
|
|
477
441
|
}
|
|
478
442
|
// Phase 2: Pull remote changes to local using fresh detection
|
|
479
443
|
const phase2Result = await this.pullRemoteChanges(freshRemoteChanges, snapshot);
|
|
@@ -492,7 +456,7 @@ class SyncEngine {
|
|
|
492
456
|
// can't find the entries to splice out).
|
|
493
457
|
for (const [filePath, snapshotEntry] of snapshot.files.entries()) {
|
|
494
458
|
try {
|
|
495
|
-
const handle = await this.repo.find(
|
|
459
|
+
const handle = await this.repo.find(getPlainUrl(snapshotEntry.url));
|
|
496
460
|
const currentHeads = handle.heads();
|
|
497
461
|
if (!A.equals(currentHeads, snapshotEntry.head)) {
|
|
498
462
|
// Update snapshot with current heads after pulling changes
|
|
@@ -509,7 +473,7 @@ class SyncEngine {
|
|
|
509
473
|
// Update directory document heads
|
|
510
474
|
for (const [dirPath, snapshotEntry] of snapshot.directories.entries()) {
|
|
511
475
|
try {
|
|
512
|
-
const handle = await this.repo.find(
|
|
476
|
+
const handle = await this.repo.find(getPlainUrl(snapshotEntry.url));
|
|
513
477
|
const currentHeads = handle.heads();
|
|
514
478
|
if (!A.equals(currentHeads, snapshotEntry.head)) {
|
|
515
479
|
// Update snapshot with current heads after pulling changes
|
|
@@ -557,13 +521,13 @@ class SyncEngine {
|
|
|
557
521
|
// Process moves first - all detected moves are applied
|
|
558
522
|
if (moves.length > 0) {
|
|
559
523
|
debug(`push: processing ${moves.length} moves`);
|
|
560
|
-
|
|
524
|
+
out.update(`Processing ${moves.length} move${moves.length > 1 ? "s" : ""}`);
|
|
561
525
|
}
|
|
562
526
|
for (let i = 0; i < moves.length; i++) {
|
|
563
527
|
const move = moves[i];
|
|
564
528
|
try {
|
|
565
529
|
debug(`push: move ${i + 1}/${moves.length}: ${move.fromPath} -> ${move.toPath}`);
|
|
566
|
-
|
|
530
|
+
out.taskLine(`Moving ${move.fromPath} -> ${move.toPath}`);
|
|
567
531
|
await this.applyMoveToRemote(move, snapshot);
|
|
568
532
|
result.filesChanged++;
|
|
569
533
|
}
|
|
@@ -578,8 +542,8 @@ class SyncEngine {
|
|
|
578
542
|
}
|
|
579
543
|
}
|
|
580
544
|
// Filter to local changes only
|
|
581
|
-
const localChanges = changes.filter(c => c.changeType ===
|
|
582
|
-
c.changeType ===
|
|
545
|
+
const localChanges = changes.filter(c => c.changeType === ChangeType.LOCAL_ONLY ||
|
|
546
|
+
c.changeType === ChangeType.BOTH_CHANGED);
|
|
583
547
|
if (localChanges.length === 0) {
|
|
584
548
|
debug("push: no local changes to push");
|
|
585
549
|
return result;
|
|
@@ -588,7 +552,7 @@ class SyncEngine {
|
|
|
588
552
|
const modifiedFiles = localChanges.filter(c => snapshot.files.has(c.path) && c.localContent !== null);
|
|
589
553
|
const deletedFiles = localChanges.filter(c => c.localContent === null && snapshot.files.has(c.path));
|
|
590
554
|
debug(`push: ${localChanges.length} local changes (${newFiles.length} new, ${modifiedFiles.length} modified, ${deletedFiles.length} deleted)`);
|
|
591
|
-
|
|
555
|
+
out.update(`Pushing ${localChanges.length} local changes (${newFiles.length} new, ${modifiedFiles.length} modified, ${deletedFiles.length} deleted)`);
|
|
592
556
|
// Group changes by parent directory path
|
|
593
557
|
const changesByDir = new Map();
|
|
594
558
|
for (const change of localChanges) {
|
|
@@ -647,7 +611,7 @@ class SyncEngine {
|
|
|
647
611
|
if (change.localContent === null && snapshotEntry) {
|
|
648
612
|
// Delete file
|
|
649
613
|
debug(`push: [${filesProcessed}/${totalFiles}] delete ${change.path}`);
|
|
650
|
-
|
|
614
|
+
out.update(`Pushing local changes [${filesProcessed}/${totalFiles}] deleting ${change.path}`);
|
|
651
615
|
await this.deleteRemoteFile(snapshotEntry.url, snapshot, change.path);
|
|
652
616
|
deletedNames.push(fileName);
|
|
653
617
|
this.snapshotManager.removeFileEntry(snapshot, change.path);
|
|
@@ -656,19 +620,19 @@ class SyncEngine {
|
|
|
656
620
|
else if (!snapshotEntry) {
|
|
657
621
|
// New file
|
|
658
622
|
debug(`push: [${filesProcessed}/${totalFiles}] create ${change.path} (${change.fileType})`);
|
|
659
|
-
|
|
623
|
+
out.update(`Pushing local changes [${filesProcessed}/${totalFiles}] creating ${change.path}`);
|
|
660
624
|
const handle = await this.createRemoteFile(change);
|
|
661
625
|
if (handle) {
|
|
662
626
|
const entryUrl = this.getEntryUrl(handle, change.path);
|
|
663
627
|
newEntries.push({ name: fileName, url: entryUrl });
|
|
664
628
|
this.snapshotManager.updateFileEntry(snapshot, change.path, {
|
|
665
|
-
path:
|
|
629
|
+
path: joinAndNormalizePath(this.rootPath, change.path),
|
|
666
630
|
url: entryUrl,
|
|
667
631
|
head: handle.heads(),
|
|
668
|
-
extension:
|
|
669
|
-
mimeType:
|
|
632
|
+
extension: getFileExtension(change.path),
|
|
633
|
+
mimeType: getEnhancedMimeType(change.path),
|
|
670
634
|
...(this.isArtifactPath(change.path) && change.localContent
|
|
671
|
-
? { contentHash:
|
|
635
|
+
? { contentHash: contentHash(change.localContent) }
|
|
672
636
|
: {}),
|
|
673
637
|
});
|
|
674
638
|
result.filesChanged++;
|
|
@@ -681,12 +645,12 @@ class SyncEngine {
|
|
|
681
645
|
? `${change.localContent.length} chars`
|
|
682
646
|
: `${change.localContent.length} bytes`;
|
|
683
647
|
debug(`push: [${filesProcessed}/${totalFiles}] update ${change.path} (${contentSize})`);
|
|
684
|
-
|
|
648
|
+
out.update(`Pushing local changes [${filesProcessed}/${totalFiles}] updating ${change.path}`);
|
|
685
649
|
await this.updateRemoteFile(snapshotEntry.url, change.localContent, snapshot, change.path);
|
|
686
650
|
// Get current entry URL (updateRemoteFile updates snapshot)
|
|
687
651
|
const updatedFileEntry = snapshot.files.get(change.path);
|
|
688
652
|
if (updatedFileEntry) {
|
|
689
|
-
const fileHandle = await this.repo.find(
|
|
653
|
+
const fileHandle = await this.repo.find(getPlainUrl(updatedFileEntry.url));
|
|
690
654
|
updatedEntries.push({
|
|
691
655
|
name: fileName,
|
|
692
656
|
url: this.getEntryUrl(fileHandle, change.path),
|
|
@@ -697,7 +661,7 @@ class SyncEngine {
|
|
|
697
661
|
}
|
|
698
662
|
catch (error) {
|
|
699
663
|
debug(`push: error processing ${change.path}: ${error}`);
|
|
700
|
-
|
|
664
|
+
out.taskLine(`Error pushing ${change.path}: ${error}`, true);
|
|
701
665
|
result.errors.push({
|
|
702
666
|
path: change.path,
|
|
703
667
|
operation: "local-to-remote",
|
|
@@ -716,7 +680,7 @@ class SyncEngine {
|
|
|
716
680
|
if (parentOfModified === dirPath) {
|
|
717
681
|
const dirEntry = snapshot.directories.get(modifiedDir);
|
|
718
682
|
if (dirEntry) {
|
|
719
|
-
const childHandle = await this.repo.find(
|
|
683
|
+
const childHandle = await this.repo.find(getPlainUrl(dirEntry.url));
|
|
720
684
|
subdirUpdates.push({
|
|
721
685
|
name: childName,
|
|
722
686
|
url: this.getEntryUrl(childHandle, modifiedDir),
|
|
@@ -751,8 +715,8 @@ class SyncEngine {
|
|
|
751
715
|
warnings: [],
|
|
752
716
|
};
|
|
753
717
|
// Process remote changes
|
|
754
|
-
const remoteChanges = changes.filter(c => c.changeType ===
|
|
755
|
-
c.changeType ===
|
|
718
|
+
const remoteChanges = changes.filter(c => c.changeType === ChangeType.REMOTE_ONLY ||
|
|
719
|
+
c.changeType === ChangeType.BOTH_CHANGED);
|
|
756
720
|
// Sort changes by dependency order (parents before children)
|
|
757
721
|
const sortedChanges = this.sortChangesByDependency(remoteChanges);
|
|
758
722
|
for (const change of sortedChanges) {
|
|
@@ -775,19 +739,19 @@ class SyncEngine {
|
|
|
775
739
|
* Apply remote change to local filesystem
|
|
776
740
|
*/
|
|
777
741
|
async applyRemoteChangeToLocal(change, snapshot) {
|
|
778
|
-
const localPath =
|
|
742
|
+
const localPath = joinAndNormalizePath(this.rootPath, change.path);
|
|
779
743
|
if (!change.remoteHead) {
|
|
780
744
|
throw new Error(`No remote head found for remote change to ${change.path}`);
|
|
781
745
|
}
|
|
782
746
|
// Check for null (empty string/Uint8Array are valid content)
|
|
783
747
|
if (change.remoteContent === null) {
|
|
784
748
|
// File was deleted remotely
|
|
785
|
-
await
|
|
749
|
+
await removePath(localPath);
|
|
786
750
|
this.snapshotManager.removeFileEntry(snapshot, change.path);
|
|
787
751
|
return;
|
|
788
752
|
}
|
|
789
753
|
// Create or update local file
|
|
790
|
-
await
|
|
754
|
+
await writeFileContent(localPath, change.remoteContent);
|
|
791
755
|
// Update or create snapshot entry for this file
|
|
792
756
|
const snapshotEntry = snapshot.files.get(change.path);
|
|
793
757
|
if (snapshotEntry) {
|
|
@@ -804,7 +768,7 @@ class SyncEngine {
|
|
|
804
768
|
// We need to find the remote file's URL from the directory hierarchy
|
|
805
769
|
if (snapshot.rootDirectoryUrl) {
|
|
806
770
|
try {
|
|
807
|
-
const fileEntry = await
|
|
771
|
+
const fileEntry = await findFileInDirectoryHierarchy(this.repo, snapshot.rootDirectoryUrl, change.path);
|
|
808
772
|
if (fileEntry) {
|
|
809
773
|
const fileHandle = await this.repo.find(fileEntry.url);
|
|
810
774
|
const entryUrl = this.getEntryUrl(fileHandle, change.path);
|
|
@@ -812,14 +776,14 @@ class SyncEngine {
|
|
|
812
776
|
path: localPath,
|
|
813
777
|
url: entryUrl,
|
|
814
778
|
head: change.remoteHead,
|
|
815
|
-
extension:
|
|
816
|
-
mimeType:
|
|
779
|
+
extension: getFileExtension(change.path),
|
|
780
|
+
mimeType: getEnhancedMimeType(change.path),
|
|
817
781
|
});
|
|
818
782
|
}
|
|
819
783
|
}
|
|
820
784
|
catch (error) {
|
|
821
785
|
// Failed to update snapshot - file may have been deleted
|
|
822
|
-
|
|
786
|
+
out.taskLine(`Warning: Failed to update snapshot for remote file ${change.path}`, true);
|
|
823
787
|
}
|
|
824
788
|
}
|
|
825
789
|
}
|
|
@@ -849,11 +813,11 @@ class SyncEngine {
|
|
|
849
813
|
// Artifact files use RawString — no diffing needed, just create a fresh doc
|
|
850
814
|
const content = move.newContent !== undefined
|
|
851
815
|
? move.newContent
|
|
852
|
-
:
|
|
816
|
+
: readDocContent((await (await this.repo.find(getPlainUrl(fromEntry.url))).doc())?.content);
|
|
853
817
|
const fakeChange = {
|
|
854
818
|
path: move.toPath,
|
|
855
|
-
changeType:
|
|
856
|
-
fileType: content != null && typeof content === "string" ?
|
|
819
|
+
changeType: ChangeType.LOCAL_ONLY,
|
|
820
|
+
fileType: content != null && typeof content === "string" ? FileType.TEXT : FileType.BINARY,
|
|
857
821
|
localContent: content,
|
|
858
822
|
remoteContent: null,
|
|
859
823
|
};
|
|
@@ -865,7 +829,7 @@ class SyncEngine {
|
|
|
865
829
|
}
|
|
866
830
|
else {
|
|
867
831
|
// Use plain URL for mutable handle
|
|
868
|
-
const handle = await this.repo.find(
|
|
832
|
+
const handle = await this.repo.find(getPlainUrl(fromEntry.url));
|
|
869
833
|
const heads = fromEntry.head;
|
|
870
834
|
// Update both name and content (if content changed during move)
|
|
871
835
|
changeWithOptionalHeads(handle, heads, (doc) => {
|
|
@@ -873,7 +837,7 @@ class SyncEngine {
|
|
|
873
837
|
// If new content is provided, update it (handles move + modification case)
|
|
874
838
|
if (move.newContent !== undefined) {
|
|
875
839
|
if (typeof move.newContent === "string") {
|
|
876
|
-
|
|
840
|
+
updateTextContent(doc, ["content"], move.newContent);
|
|
877
841
|
}
|
|
878
842
|
else {
|
|
879
843
|
doc.content = move.newContent;
|
|
@@ -891,17 +855,17 @@ class SyncEngine {
|
|
|
891
855
|
this.snapshotManager.removeFileEntry(snapshot, move.fromPath);
|
|
892
856
|
this.snapshotManager.updateFileEntry(snapshot, move.toPath, {
|
|
893
857
|
...fromEntry,
|
|
894
|
-
path:
|
|
858
|
+
path: joinAndNormalizePath(this.rootPath, move.toPath),
|
|
895
859
|
url: entryUrl,
|
|
896
860
|
head: finalHeads,
|
|
897
861
|
...(this.isArtifactPath(move.toPath) && move.newContent != null
|
|
898
|
-
? { contentHash:
|
|
862
|
+
? { contentHash: contentHash(move.newContent) }
|
|
899
863
|
: {}),
|
|
900
864
|
});
|
|
901
865
|
}
|
|
902
866
|
catch (e) {
|
|
903
867
|
// Failed to update file name - file may have been deleted
|
|
904
|
-
|
|
868
|
+
out.taskLine(`Warning: Failed to rename ${move.fromPath} to ${move.toPath}`, true);
|
|
905
869
|
}
|
|
906
870
|
}
|
|
907
871
|
/**
|
|
@@ -917,8 +881,8 @@ class SyncEngine {
|
|
|
917
881
|
const fileDoc = {
|
|
918
882
|
"@patchwork": { type: "file" },
|
|
919
883
|
name: change.path.split("/").pop() || "",
|
|
920
|
-
extension:
|
|
921
|
-
mimeType:
|
|
884
|
+
extension: getFileExtension(change.path),
|
|
885
|
+
mimeType: getEnhancedMimeType(change.path),
|
|
922
886
|
content: isText && isArtifact
|
|
923
887
|
? new A.RawString(change.localContent)
|
|
924
888
|
: isText
|
|
@@ -932,7 +896,7 @@ class SyncEngine {
|
|
|
932
896
|
// For non-artifact text files, splice in the content so it's stored as collaborative text
|
|
933
897
|
if (isText && !isArtifact && typeof change.localContent === "string") {
|
|
934
898
|
handle.change((doc) => {
|
|
935
|
-
|
|
899
|
+
updateTextContent(doc, ["content"], change.localContent);
|
|
936
900
|
});
|
|
937
901
|
}
|
|
938
902
|
// Always track newly created files for network sync
|
|
@@ -945,7 +909,7 @@ class SyncEngine {
|
|
|
945
909
|
*/
|
|
946
910
|
async updateRemoteFile(url, content, snapshot, filePath) {
|
|
947
911
|
// Use plain URL for mutable handle
|
|
948
|
-
const handle = await this.repo.find(
|
|
912
|
+
const handle = await this.repo.find(getPlainUrl(url));
|
|
949
913
|
// Check if content actually changed before tracking for sync
|
|
950
914
|
const doc = await handle.doc();
|
|
951
915
|
const rawContent = doc?.content;
|
|
@@ -957,14 +921,14 @@ class SyncEngine {
|
|
|
957
921
|
!doc ||
|
|
958
922
|
(rawContent != null && A.isImmutableString(rawContent))) {
|
|
959
923
|
if (!isArtifact) {
|
|
960
|
-
|
|
924
|
+
out.taskLine(`Replacing ${!doc ? 'unavailable' : 'immutable string'} document for ${filePath}`, true);
|
|
961
925
|
}
|
|
962
926
|
const fakeChange = {
|
|
963
927
|
path: filePath,
|
|
964
|
-
changeType:
|
|
928
|
+
changeType: ChangeType.LOCAL_ONLY,
|
|
965
929
|
fileType: this.isTextContent(content)
|
|
966
|
-
?
|
|
967
|
-
:
|
|
930
|
+
? FileType.TEXT
|
|
931
|
+
: FileType.BINARY,
|
|
968
932
|
localContent: content,
|
|
969
933
|
remoteContent: null,
|
|
970
934
|
};
|
|
@@ -972,20 +936,20 @@ class SyncEngine {
|
|
|
972
936
|
if (newHandle) {
|
|
973
937
|
const entryUrl = this.getEntryUrl(newHandle, filePath);
|
|
974
938
|
this.snapshotManager.updateFileEntry(snapshot, filePath, {
|
|
975
|
-
path:
|
|
939
|
+
path: joinAndNormalizePath(this.rootPath, filePath),
|
|
976
940
|
url: entryUrl,
|
|
977
941
|
head: newHandle.heads(),
|
|
978
|
-
extension:
|
|
979
|
-
mimeType:
|
|
942
|
+
extension: getFileExtension(filePath),
|
|
943
|
+
mimeType: getEnhancedMimeType(filePath),
|
|
980
944
|
...(this.isArtifactPath(filePath)
|
|
981
|
-
? { contentHash:
|
|
945
|
+
? { contentHash: contentHash(content) }
|
|
982
946
|
: {}),
|
|
983
947
|
});
|
|
984
948
|
}
|
|
985
949
|
return;
|
|
986
950
|
}
|
|
987
|
-
const currentContent =
|
|
988
|
-
const contentChanged = !
|
|
951
|
+
const currentContent = readDocContent(rawContent);
|
|
952
|
+
const contentChanged = !isContentEqual(content, currentContent);
|
|
989
953
|
// Update snapshot heads even when content is identical
|
|
990
954
|
const snapshotEntry = snapshot.files.get(filePath);
|
|
991
955
|
if (snapshotEntry) {
|
|
@@ -1006,7 +970,7 @@ class SyncEngine {
|
|
|
1006
970
|
}
|
|
1007
971
|
handle.changeAt(heads, (doc) => {
|
|
1008
972
|
if (typeof content === "string") {
|
|
1009
|
-
|
|
973
|
+
updateTextContent(doc, ["content"], content);
|
|
1010
974
|
}
|
|
1011
975
|
else {
|
|
1012
976
|
doc.content = content;
|
|
@@ -1043,7 +1007,7 @@ class SyncEngine {
|
|
|
1043
1007
|
// Get or create the parent directory document
|
|
1044
1008
|
const parentDirUrl = await this.ensureDirectoryDocument(snapshot, directoryPath);
|
|
1045
1009
|
// Use plain URL for mutable handle
|
|
1046
|
-
const dirHandle = await this.repo.find(
|
|
1010
|
+
const dirHandle = await this.repo.find(getPlainUrl(parentDirUrl));
|
|
1047
1011
|
let didChange = false;
|
|
1048
1012
|
const snapshotEntry = snapshot.directories.get(directoryPath);
|
|
1049
1013
|
const heads = snapshotEntry?.head;
|
|
@@ -1101,7 +1065,7 @@ class SyncEngine {
|
|
|
1101
1065
|
const entryUrl = this.getEntryUrl(childDirHandle, directoryPath);
|
|
1102
1066
|
// Update snapshot with discovered directory
|
|
1103
1067
|
this.snapshotManager.updateDirectoryEntry(snapshot, directoryPath, {
|
|
1104
|
-
path:
|
|
1068
|
+
path: joinAndNormalizePath(this.rootPath, directoryPath),
|
|
1105
1069
|
url: entryUrl,
|
|
1106
1070
|
head: childDirHandle.heads(),
|
|
1107
1071
|
entries: [],
|
|
@@ -1129,7 +1093,7 @@ class SyncEngine {
|
|
|
1129
1093
|
const dirEntryUrl = this.getEntryUrl(dirHandle, directoryPath);
|
|
1130
1094
|
// Add this directory to its parent
|
|
1131
1095
|
// Use plain URL for mutable handle
|
|
1132
|
-
const parentHandle = await this.repo.find(
|
|
1096
|
+
const parentHandle = await this.repo.find(getPlainUrl(parentDirUrl));
|
|
1133
1097
|
let didChange = false;
|
|
1134
1098
|
parentHandle.change((doc) => {
|
|
1135
1099
|
// Double-check that entry doesn't exist (race condition protection)
|
|
@@ -1154,7 +1118,7 @@ class SyncEngine {
|
|
|
1154
1118
|
}
|
|
1155
1119
|
// Update snapshot with new directory
|
|
1156
1120
|
this.snapshotManager.updateDirectoryEntry(snapshot, directoryPath, {
|
|
1157
|
-
path:
|
|
1121
|
+
path: joinAndNormalizePath(this.rootPath, directoryPath),
|
|
1158
1122
|
url: dirEntryUrl,
|
|
1159
1123
|
head: dirHandle.heads(),
|
|
1160
1124
|
entries: [],
|
|
@@ -1185,7 +1149,7 @@ class SyncEngine {
|
|
|
1185
1149
|
}
|
|
1186
1150
|
try {
|
|
1187
1151
|
// Use plain URL for mutable handle
|
|
1188
|
-
const dirHandle = await this.repo.find(
|
|
1152
|
+
const dirHandle = await this.repo.find(getPlainUrl(parentDirUrl));
|
|
1189
1153
|
// Track this handle for network sync waiting
|
|
1190
1154
|
this.handlesByPath.set(directoryPath, dirHandle);
|
|
1191
1155
|
const snapshotEntry = snapshot.directories.get(directoryPath);
|
|
@@ -1196,7 +1160,7 @@ class SyncEngine {
|
|
|
1196
1160
|
if (indexToRemove !== -1) {
|
|
1197
1161
|
doc.docs.splice(indexToRemove, 1);
|
|
1198
1162
|
didChange = true;
|
|
1199
|
-
|
|
1163
|
+
out.taskLine(`Removed ${fileName} from ${formatRelativePath(directoryPath) || "root"}`);
|
|
1200
1164
|
}
|
|
1201
1165
|
});
|
|
1202
1166
|
if (didChange && snapshotEntry) {
|
|
@@ -1224,7 +1188,7 @@ class SyncEngine {
|
|
|
1224
1188
|
return;
|
|
1225
1189
|
dirUrl = dirEntry.url;
|
|
1226
1190
|
}
|
|
1227
|
-
const dirHandle = await this.repo.find(
|
|
1191
|
+
const dirHandle = await this.repo.find(getPlainUrl(dirUrl));
|
|
1228
1192
|
const snapshotEntry = snapshot.directories.get(dirPath);
|
|
1229
1193
|
const heads = snapshotEntry?.head;
|
|
1230
1194
|
// Determine directory name
|
|
@@ -1240,7 +1204,7 @@ class SyncEngine {
|
|
|
1240
1204
|
const idx = doc.docs.findIndex(entry => entry.name === name && entry.type === "file");
|
|
1241
1205
|
if (idx !== -1) {
|
|
1242
1206
|
doc.docs.splice(idx, 1);
|
|
1243
|
-
|
|
1207
|
+
out.taskLine(`Removed ${name} from ${formatRelativePath(dirPath) || "root"}`);
|
|
1244
1208
|
}
|
|
1245
1209
|
}
|
|
1246
1210
|
// Update URLs for modified files
|
|
@@ -1328,11 +1292,11 @@ class SyncEngine {
|
|
|
1328
1292
|
* Generate human-readable summary of changes
|
|
1329
1293
|
*/
|
|
1330
1294
|
generateChangeSummary(changes, moves) {
|
|
1331
|
-
const localChanges = changes.filter(c => c.changeType ===
|
|
1332
|
-
c.changeType ===
|
|
1333
|
-
const remoteChanges = changes.filter(c => c.changeType ===
|
|
1334
|
-
c.changeType ===
|
|
1335
|
-
const conflicts = changes.filter(c => c.changeType ===
|
|
1295
|
+
const localChanges = changes.filter(c => c.changeType === ChangeType.LOCAL_ONLY ||
|
|
1296
|
+
c.changeType === ChangeType.BOTH_CHANGED).length;
|
|
1297
|
+
const remoteChanges = changes.filter(c => c.changeType === ChangeType.REMOTE_ONLY ||
|
|
1298
|
+
c.changeType === ChangeType.BOTH_CHANGED).length;
|
|
1299
|
+
const conflicts = changes.filter(c => c.changeType === ChangeType.BOTH_CHANGED).length;
|
|
1336
1300
|
const parts = [];
|
|
1337
1301
|
if (localChanges > 0) {
|
|
1338
1302
|
parts.push(`${localChanges} local change${localChanges > 1 ? "s" : ""}`);
|
|
@@ -1379,5 +1343,4 @@ class SyncEngine {
|
|
|
1379
1343
|
}
|
|
1380
1344
|
}
|
|
1381
1345
|
}
|
|
1382
|
-
exports.SyncEngine = SyncEngine;
|
|
1383
1346
|
//# sourceMappingURL=sync-engine.js.map
|