pushwork 1.0.20 → 1.0.22
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/dist/cli.js +11 -0
- 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 +41 -1
- package/dist/commands.js.map +1 -1
- package/dist/core/sync-engine.d.ts +5 -0
- package/dist/core/sync-engine.d.ts.map +1 -1
- package/dist/core/sync-engine.js +128 -4
- package/dist/core/sync-engine.js.map +1 -1
- package/dist/types/config.d.ts +5 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/utils/network-sync.d.ts +1 -0
- package/dist/utils/network-sync.d.ts.map +1 -1
- package/dist/utils/network-sync.js +70 -14
- package/dist/utils/network-sync.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +17 -0
- package/src/commands.ts +54 -1
- package/src/core/sync-engine.ts +162 -4
- package/src/types/config.ts +5 -0
- package/src/utils/network-sync.ts +75 -15
package/src/core/sync-engine.ts
CHANGED
|
@@ -248,6 +248,111 @@ export class SyncEngine {
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Push local changes to server without pulling remote changes.
|
|
253
|
+
* Detect changes, push to Automerge docs, upload to server. No bidirectional wait, no pull.
|
|
254
|
+
*/
|
|
255
|
+
async pushToRemote(): Promise<SyncResult> {
|
|
256
|
+
const result: SyncResult = {
|
|
257
|
+
success: false,
|
|
258
|
+
filesChanged: 0,
|
|
259
|
+
directoriesChanged: 0,
|
|
260
|
+
errors: [],
|
|
261
|
+
warnings: [],
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Reset tracked handles for sync
|
|
265
|
+
this.handlesByPath = new Map()
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const snapshot =
|
|
269
|
+
(await this.snapshotManager.load()) ||
|
|
270
|
+
this.snapshotManager.createEmpty()
|
|
271
|
+
|
|
272
|
+
debug(`pushToRemote: rootDirectoryUrl=${snapshot.rootDirectoryUrl?.slice(0, 30)}..., files=${snapshot.files.size}, dirs=${snapshot.directories.size}`)
|
|
273
|
+
|
|
274
|
+
// Detect all changes
|
|
275
|
+
debug("pushToRemote: detecting changes")
|
|
276
|
+
out.update("Detecting local changes")
|
|
277
|
+
const changes = await this.changeDetector.detectChanges(snapshot)
|
|
278
|
+
|
|
279
|
+
// Detect moves
|
|
280
|
+
const {moves, remainingChanges} = await this.moveDetector.detectMoves(
|
|
281
|
+
changes,
|
|
282
|
+
snapshot
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
debug(`pushToRemote: detected ${changes.length} changes, ${moves.length} moves, ${remainingChanges.length} remaining`)
|
|
286
|
+
|
|
287
|
+
// Push local changes to remote
|
|
288
|
+
debug("pushToRemote: pushing local changes")
|
|
289
|
+
const pushResult = await this.pushLocalChanges(
|
|
290
|
+
remainingChanges,
|
|
291
|
+
moves,
|
|
292
|
+
snapshot
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
result.filesChanged += pushResult.filesChanged
|
|
296
|
+
result.directoriesChanged += pushResult.directoriesChanged
|
|
297
|
+
result.errors.push(...pushResult.errors)
|
|
298
|
+
result.warnings.push(...pushResult.warnings)
|
|
299
|
+
|
|
300
|
+
debug(`pushToRemote: push complete - ${pushResult.filesChanged} files, ${pushResult.directoriesChanged} dirs changed`)
|
|
301
|
+
|
|
302
|
+
// Touch root directory if any changes were made
|
|
303
|
+
const hasChanges =
|
|
304
|
+
result.filesChanged > 0 || result.directoriesChanged > 0
|
|
305
|
+
if (hasChanges) {
|
|
306
|
+
await this.touchRootDirectory(snapshot)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Wait for network sync (upload to server)
|
|
310
|
+
if (this.config.sync_enabled) {
|
|
311
|
+
try {
|
|
312
|
+
// Ensure root directory handle is tracked for sync
|
|
313
|
+
if (snapshot.rootDirectoryUrl) {
|
|
314
|
+
const rootHandle =
|
|
315
|
+
await this.repo.find<DirectoryDocument>(
|
|
316
|
+
snapshot.rootDirectoryUrl
|
|
317
|
+
)
|
|
318
|
+
this.handlesByPath.set("", rootHandle)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (this.handlesByPath.size > 0) {
|
|
322
|
+
const allHandles = Array.from(
|
|
323
|
+
this.handlesByPath.values()
|
|
324
|
+
)
|
|
325
|
+
debug(`pushToRemote: waiting for ${allHandles.length} handles to sync to server`)
|
|
326
|
+
out.update(`Uploading ${allHandles.length} documents to sync server`)
|
|
327
|
+
await waitForSync(
|
|
328
|
+
allHandles,
|
|
329
|
+
this.config.sync_server_storage_id
|
|
330
|
+
)
|
|
331
|
+
debug("pushToRemote: all handles synced to server")
|
|
332
|
+
}
|
|
333
|
+
} catch (error) {
|
|
334
|
+
debug(`pushToRemote: network sync error: ${error}`)
|
|
335
|
+
out.taskLine(`Network sync failed: ${error}`, true)
|
|
336
|
+
result.warnings.push(`Network sync failed: ${error}`)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Save updated snapshot
|
|
341
|
+
await this.snapshotManager.save(snapshot)
|
|
342
|
+
|
|
343
|
+
result.success = result.errors.length === 0
|
|
344
|
+
return result
|
|
345
|
+
} catch (error) {
|
|
346
|
+
result.errors.push({
|
|
347
|
+
path: "push",
|
|
348
|
+
operation: "push-to-remote",
|
|
349
|
+
error: error as Error,
|
|
350
|
+
recoverable: false,
|
|
351
|
+
})
|
|
352
|
+
return result
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
251
356
|
/**
|
|
252
357
|
* Run full bidirectional sync
|
|
253
358
|
*/
|
|
@@ -275,6 +380,7 @@ export class SyncEngine {
|
|
|
275
380
|
// Wait for initial sync to receive any pending remote changes
|
|
276
381
|
if (this.config.sync_enabled && snapshot.rootDirectoryUrl) {
|
|
277
382
|
debug("sync: waiting for initial bidirectional sync")
|
|
383
|
+
out.update("Waiting for initial sync from server")
|
|
278
384
|
try {
|
|
279
385
|
await waitForBidirectionalSync(
|
|
280
386
|
this.repo,
|
|
@@ -293,6 +399,7 @@ export class SyncEngine {
|
|
|
293
399
|
|
|
294
400
|
// Detect all changes
|
|
295
401
|
debug("sync: detecting changes")
|
|
402
|
+
out.update("Detecting local and remote changes")
|
|
296
403
|
const changes = await this.changeDetector.detectChanges(snapshot)
|
|
297
404
|
|
|
298
405
|
// Detect moves
|
|
@@ -335,15 +442,21 @@ export class SyncEngine {
|
|
|
335
442
|
const allHandles = Array.from(
|
|
336
443
|
this.handlesByPath.values()
|
|
337
444
|
)
|
|
338
|
-
|
|
445
|
+
const handlePaths = Array.from(this.handlesByPath.keys())
|
|
446
|
+
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` : ""}`)
|
|
447
|
+
out.update(`Uploading ${allHandles.length} documents to sync server`)
|
|
339
448
|
await waitForSync(
|
|
340
449
|
allHandles,
|
|
341
450
|
this.config.sync_server_storage_id
|
|
342
451
|
)
|
|
452
|
+
debug("sync: all handles synced to server")
|
|
343
453
|
}
|
|
344
454
|
|
|
345
455
|
// Wait for bidirectional sync to stabilize
|
|
346
|
-
|
|
456
|
+
// Use tracked handles for post-push check (cheaper than full tree scan)
|
|
457
|
+
const changedHandles = Array.from(this.handlesByPath.values())
|
|
458
|
+
debug(`sync: waiting for bidirectional sync to stabilize (${changedHandles.length} tracked handles)`)
|
|
459
|
+
out.update("Waiting for bidirectional sync to stabilize")
|
|
347
460
|
await waitForBidirectionalSync(
|
|
348
461
|
this.repo,
|
|
349
462
|
snapshot.rootDirectoryUrl,
|
|
@@ -352,6 +465,7 @@ export class SyncEngine {
|
|
|
352
465
|
timeoutMs: BIDIRECTIONAL_SYNC_TIMEOUT_MS,
|
|
353
466
|
pollIntervalMs: 100,
|
|
354
467
|
stableChecksRequired: 3,
|
|
468
|
+
handles: changedHandles.length > 0 ? changedHandles : undefined,
|
|
355
469
|
}
|
|
356
470
|
)
|
|
357
471
|
} catch (error) {
|
|
@@ -371,6 +485,9 @@ export class SyncEngine {
|
|
|
371
485
|
)
|
|
372
486
|
|
|
373
487
|
debug(`sync: phase 2 - pulling ${freshRemoteChanges.length} remote changes`)
|
|
488
|
+
if (freshRemoteChanges.length > 0) {
|
|
489
|
+
out.update(`Pulling ${freshRemoteChanges.length} remote changes`)
|
|
490
|
+
}
|
|
374
491
|
// Phase 2: Pull remote changes to local using fresh detection
|
|
375
492
|
const phase2Result = await this.pullRemoteChanges(
|
|
376
493
|
freshRemoteChanges,
|
|
@@ -460,11 +577,19 @@ export class SyncEngine {
|
|
|
460
577
|
}
|
|
461
578
|
|
|
462
579
|
// Process moves first - all detected moves are applied
|
|
463
|
-
|
|
580
|
+
if (moves.length > 0) {
|
|
581
|
+
debug(`push: processing ${moves.length} moves`)
|
|
582
|
+
out.update(`Processing ${moves.length} move${moves.length > 1 ? "s" : ""}`)
|
|
583
|
+
}
|
|
584
|
+
for (let i = 0; i < moves.length; i++) {
|
|
585
|
+
const move = moves[i]
|
|
464
586
|
try {
|
|
587
|
+
debug(`push: move ${i + 1}/${moves.length}: ${move.fromPath} -> ${move.toPath}`)
|
|
588
|
+
out.taskLine(`Moving ${move.fromPath} -> ${move.toPath}`)
|
|
465
589
|
await this.applyMoveToRemote(move, snapshot)
|
|
466
590
|
result.filesChanged++
|
|
467
591
|
} catch (error) {
|
|
592
|
+
debug(`push: move failed for ${move.fromPath}: ${error}`)
|
|
468
593
|
result.errors.push({
|
|
469
594
|
path: move.fromPath,
|
|
470
595
|
operation: "move",
|
|
@@ -481,7 +606,16 @@ export class SyncEngine {
|
|
|
481
606
|
c.changeType === ChangeType.BOTH_CHANGED
|
|
482
607
|
)
|
|
483
608
|
|
|
484
|
-
if (localChanges.length === 0)
|
|
609
|
+
if (localChanges.length === 0) {
|
|
610
|
+
debug("push: no local changes to push")
|
|
611
|
+
return result
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const newFiles = localChanges.filter(c => !snapshot.files.has(c.path) && c.localContent !== null)
|
|
615
|
+
const modifiedFiles = localChanges.filter(c => snapshot.files.has(c.path) && c.localContent !== null)
|
|
616
|
+
const deletedFiles = localChanges.filter(c => c.localContent === null && snapshot.files.has(c.path))
|
|
617
|
+
debug(`push: ${localChanges.length} local changes (${newFiles.length} new, ${modifiedFiles.length} modified, ${deletedFiles.length} deleted)`)
|
|
618
|
+
out.update(`Pushing ${localChanges.length} local changes (${newFiles.length} new, ${modifiedFiles.length} modified, ${deletedFiles.length} deleted)`)
|
|
485
619
|
|
|
486
620
|
// Group changes by parent directory path
|
|
487
621
|
const changesByDir = new Map<string, DetectedChange[]>()
|
|
@@ -517,11 +651,20 @@ export class SyncEngine {
|
|
|
517
651
|
return depthB - depthA
|
|
518
652
|
})
|
|
519
653
|
|
|
654
|
+
debug(`push: processing ${sortedDirPaths.length} directories (deepest first)`)
|
|
655
|
+
|
|
520
656
|
// Track which directories were modified (for subdirectory URL propagation)
|
|
521
657
|
const modifiedDirs = new Set<string>()
|
|
658
|
+
let filesProcessed = 0
|
|
659
|
+
const totalFiles = localChanges.length
|
|
522
660
|
|
|
523
661
|
for (const dirPath of sortedDirPaths) {
|
|
524
662
|
const dirChanges = changesByDir.get(dirPath) || []
|
|
663
|
+
const dirLabel = dirPath || "(root)"
|
|
664
|
+
|
|
665
|
+
if (dirChanges.length > 0) {
|
|
666
|
+
debug(`push: directory "${dirLabel}": ${dirChanges.length} file changes`)
|
|
667
|
+
}
|
|
525
668
|
|
|
526
669
|
// Ensure directory document exists
|
|
527
670
|
if (snapshot.rootDirectoryUrl) {
|
|
@@ -536,10 +679,13 @@ export class SyncEngine {
|
|
|
536
679
|
for (const change of dirChanges) {
|
|
537
680
|
const fileName = change.path.split("/").pop() || ""
|
|
538
681
|
const snapshotEntry = snapshot.files.get(change.path)
|
|
682
|
+
filesProcessed++
|
|
539
683
|
|
|
540
684
|
try {
|
|
541
685
|
if (change.localContent === null && snapshotEntry) {
|
|
542
686
|
// Delete file
|
|
687
|
+
debug(`push: [${filesProcessed}/${totalFiles}] delete ${change.path}`)
|
|
688
|
+
out.update(`Pushing local changes [${filesProcessed}/${totalFiles}] deleting ${change.path}`)
|
|
543
689
|
await this.deleteRemoteFile(
|
|
544
690
|
snapshotEntry.url,
|
|
545
691
|
snapshot,
|
|
@@ -550,6 +696,8 @@ export class SyncEngine {
|
|
|
550
696
|
result.filesChanged++
|
|
551
697
|
} else if (!snapshotEntry) {
|
|
552
698
|
// New file
|
|
699
|
+
debug(`push: [${filesProcessed}/${totalFiles}] create ${change.path} (${change.fileType})`)
|
|
700
|
+
out.update(`Pushing local changes [${filesProcessed}/${totalFiles}] creating ${change.path}`)
|
|
553
701
|
const handle = await this.createRemoteFile(change)
|
|
554
702
|
if (handle) {
|
|
555
703
|
const entryUrl = this.getEntryUrl(handle, change.path)
|
|
@@ -569,9 +717,15 @@ export class SyncEngine {
|
|
|
569
717
|
}
|
|
570
718
|
)
|
|
571
719
|
result.filesChanged++
|
|
720
|
+
debug(`push: created ${change.path} -> ${handle.url.slice(0, 20)}...`)
|
|
572
721
|
}
|
|
573
722
|
} else {
|
|
574
723
|
// Update existing file
|
|
724
|
+
const contentSize = typeof change.localContent === "string"
|
|
725
|
+
? `${change.localContent!.length} chars`
|
|
726
|
+
: `${(change.localContent as Uint8Array).length} bytes`
|
|
727
|
+
debug(`push: [${filesProcessed}/${totalFiles}] update ${change.path} (${contentSize})`)
|
|
728
|
+
out.update(`Pushing local changes [${filesProcessed}/${totalFiles}] updating ${change.path}`)
|
|
575
729
|
await this.updateRemoteFile(
|
|
576
730
|
snapshotEntry.url,
|
|
577
731
|
change.localContent!,
|
|
@@ -593,6 +747,8 @@ export class SyncEngine {
|
|
|
593
747
|
result.filesChanged++
|
|
594
748
|
}
|
|
595
749
|
} catch (error) {
|
|
750
|
+
debug(`push: error processing ${change.path}: ${error}`)
|
|
751
|
+
out.taskLine(`Error pushing ${change.path}: ${error}`, true)
|
|
596
752
|
result.errors.push({
|
|
597
753
|
path: change.path,
|
|
598
754
|
operation: "local-to-remote",
|
|
@@ -631,6 +787,7 @@ export class SyncEngine {
|
|
|
631
787
|
deletedNames.length > 0 ||
|
|
632
788
|
subdirUpdates.length > 0
|
|
633
789
|
if (hasChanges && snapshot.rootDirectoryUrl) {
|
|
790
|
+
debug(`push: batch-updating directory "${dirLabel}" (+${newEntries.length} new, ~${updatedEntries.length} updated, -${deletedNames.length} deleted, ${subdirUpdates.length} subdir URL updates)`)
|
|
634
791
|
await this.batchUpdateDirectory(
|
|
635
792
|
snapshot,
|
|
636
793
|
dirPath,
|
|
@@ -644,6 +801,7 @@ export class SyncEngine {
|
|
|
644
801
|
}
|
|
645
802
|
}
|
|
646
803
|
|
|
804
|
+
debug(`push: complete - ${result.filesChanged} files, ${result.directoriesChanged} dirs changed, ${result.errors.length} errors`)
|
|
647
805
|
return result
|
|
648
806
|
}
|
|
649
807
|
|
package/src/types/config.ts
CHANGED
|
@@ -101,6 +101,11 @@ export interface StatusOptions extends CommandOptions {
|
|
|
101
101
|
verbose?: boolean;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Push command specific options
|
|
106
|
+
*/
|
|
107
|
+
export interface PushOptions extends CommandOptions {}
|
|
108
|
+
|
|
104
109
|
/**
|
|
105
110
|
* Watch command specific options
|
|
106
111
|
*/
|
|
@@ -32,12 +32,14 @@ export async function waitForBidirectionalSync(
|
|
|
32
32
|
timeoutMs?: number;
|
|
33
33
|
pollIntervalMs?: number;
|
|
34
34
|
stableChecksRequired?: number;
|
|
35
|
+
handles?: DocHandle<unknown>[];
|
|
35
36
|
} = {},
|
|
36
37
|
): Promise<void> {
|
|
37
38
|
const {
|
|
38
39
|
timeoutMs = 10000,
|
|
39
40
|
pollIntervalMs = 100,
|
|
40
41
|
stableChecksRequired = 3,
|
|
42
|
+
handles,
|
|
41
43
|
} = options;
|
|
42
44
|
|
|
43
45
|
if (!syncServerStorageId || !rootDirectoryUrl) {
|
|
@@ -47,26 +49,58 @@ export async function waitForBidirectionalSync(
|
|
|
47
49
|
const startTime = Date.now();
|
|
48
50
|
let lastSeenHeads = new Map<string, string>();
|
|
49
51
|
let stableCount = 0;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
let pollCount = 0;
|
|
53
|
+
let dynamicTimeoutMs = timeoutMs;
|
|
54
|
+
|
|
55
|
+
debug(`waitForBidirectionalSync: starting (timeout=${timeoutMs}ms, stableChecks=${stableChecksRequired}${handles ? `, tracking ${handles.length} handles` : ', full tree scan'})`);
|
|
56
|
+
|
|
57
|
+
while (Date.now() - startTime < dynamicTimeoutMs) {
|
|
58
|
+
pollCount++;
|
|
59
|
+
// Get current heads: use provided handles if available, otherwise full tree scan
|
|
60
|
+
const currentHeads = handles
|
|
61
|
+
? getHandleHeads(handles)
|
|
62
|
+
: await getAllDocumentHeads(repo, rootDirectoryUrl);
|
|
63
|
+
|
|
64
|
+
// Scale timeout proportionally to tree size after first scan
|
|
65
|
+
if (pollCount === 1) {
|
|
66
|
+
dynamicTimeoutMs = Math.max(timeoutMs, 5000 + currentHeads.size * 50);
|
|
67
|
+
if (dynamicTimeoutMs !== timeoutMs) {
|
|
68
|
+
debug(`waitForBidirectionalSync: scaled timeout to ${dynamicTimeoutMs}ms for ${currentHeads.size} docs`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
56
71
|
|
|
57
72
|
// Check if heads are stable (no changes since last check)
|
|
58
73
|
const isStable = headsMapEqual(lastSeenHeads, currentHeads);
|
|
59
74
|
|
|
60
75
|
if (isStable) {
|
|
61
76
|
stableCount++;
|
|
62
|
-
debug(`waitForBidirectionalSync: stable check ${stableCount}/${stableChecksRequired} (${currentHeads.size} docs)`);
|
|
77
|
+
debug(`waitForBidirectionalSync: stable check ${stableCount}/${stableChecksRequired} (${currentHeads.size} docs, poll #${pollCount})`);
|
|
63
78
|
if (stableCount >= stableChecksRequired) {
|
|
64
|
-
|
|
79
|
+
const elapsed = Date.now() - startTime;
|
|
80
|
+
debug(`waitForBidirectionalSync: converged in ${elapsed}ms after ${pollCount} polls (${currentHeads.size} docs)`);
|
|
81
|
+
out.taskLine(`Bidirectional sync converged (${currentHeads.size} docs, ${elapsed}ms)`);
|
|
65
82
|
return; // Converged!
|
|
66
83
|
}
|
|
67
84
|
} else {
|
|
85
|
+
// Find which docs changed
|
|
86
|
+
if (lastSeenHeads.size > 0) {
|
|
87
|
+
const changedDocs: string[] = [];
|
|
88
|
+
for (const [url, heads] of currentHeads) {
|
|
89
|
+
if (lastSeenHeads.get(url) !== heads) {
|
|
90
|
+
changedDocs.push(url.slice(0, 20) + "...");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const newDocs = currentHeads.size - lastSeenHeads.size;
|
|
94
|
+
if (newDocs > 0) {
|
|
95
|
+
debug(`waitForBidirectionalSync: ${newDocs} new docs discovered, ${changedDocs.length} docs changed heads (poll #${pollCount})`);
|
|
96
|
+
} else if (changedDocs.length > 0) {
|
|
97
|
+
debug(`waitForBidirectionalSync: ${changedDocs.length} docs changed heads: ${changedDocs.slice(0, 5).join(", ")}${changedDocs.length > 5 ? ` ...and ${changedDocs.length - 5} more` : ""} (poll #${pollCount})`);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
debug(`waitForBidirectionalSync: initial scan found ${currentHeads.size} docs (poll #${pollCount})`);
|
|
101
|
+
}
|
|
68
102
|
if (stableCount > 0) {
|
|
69
|
-
debug(`waitForBidirectionalSync: heads changed
|
|
103
|
+
debug(`waitForBidirectionalSync: heads changed after ${stableCount} stable checks, resetting`);
|
|
70
104
|
}
|
|
71
105
|
stableCount = 0;
|
|
72
106
|
lastSeenHeads = currentHeads;
|
|
@@ -77,8 +111,23 @@ export async function waitForBidirectionalSync(
|
|
|
77
111
|
|
|
78
112
|
// Timeout - but don't throw, just log a warning
|
|
79
113
|
// The sync may still work, we just couldn't confirm stability
|
|
80
|
-
|
|
81
|
-
|
|
114
|
+
const elapsed = Date.now() - startTime;
|
|
115
|
+
debug(`waitForBidirectionalSync: timed out after ${elapsed}ms (${pollCount} polls, ${lastSeenHeads.size} docs tracked, reached ${stableCount}/${stableChecksRequired} stable checks)`);
|
|
116
|
+
out.taskLine(`Bidirectional sync timed out after ${(elapsed / 1000).toFixed(1)}s - document heads were still changing after ${pollCount} checks across ${lastSeenHeads.size} docs (reached ${stableCount}/${stableChecksRequired} stability checks). This may mean another peer is actively editing, or the sync server is slow to relay changes. The sync will continue but some remote changes may not be reflected yet.`, true);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get heads from a pre-collected set of handles (cheap, synchronous reads).
|
|
121
|
+
* Used for post-push stabilization where we already know which documents changed.
|
|
122
|
+
*/
|
|
123
|
+
function getHandleHeads(
|
|
124
|
+
handles: DocHandle<unknown>[],
|
|
125
|
+
): Map<string, string> {
|
|
126
|
+
const heads = new Map<string, string>();
|
|
127
|
+
for (const handle of handles) {
|
|
128
|
+
heads.set(getPlainUrl(handle.url), JSON.stringify(handle.heads()));
|
|
129
|
+
}
|
|
130
|
+
return heads;
|
|
82
131
|
}
|
|
83
132
|
|
|
84
133
|
/**
|
|
@@ -117,8 +166,8 @@ async function collectHeadsRecursive(
|
|
|
117
166
|
return;
|
|
118
167
|
}
|
|
119
168
|
|
|
120
|
-
// Process all entries in the directory
|
|
121
|
-
|
|
169
|
+
// Process all entries in the directory concurrently
|
|
170
|
+
await Promise.all(doc.docs.map(async (entry: { type: string; url: AutomergeUrl; name: string }) => {
|
|
122
171
|
if (entry.type === "folder") {
|
|
123
172
|
// Recurse into subdirectory (entry.url may have stale heads)
|
|
124
173
|
await collectHeadsRecursive(repo, entry.url, heads);
|
|
@@ -132,7 +181,7 @@ async function collectHeadsRecursive(
|
|
|
132
181
|
// File document may not exist yet
|
|
133
182
|
}
|
|
134
183
|
}
|
|
135
|
-
}
|
|
184
|
+
}));
|
|
136
185
|
} catch {
|
|
137
186
|
// Directory may not exist yet
|
|
138
187
|
}
|
|
@@ -260,12 +309,23 @@ export async function waitForSync(
|
|
|
260
309
|
});
|
|
261
310
|
});
|
|
262
311
|
|
|
312
|
+
const needSync = handlesToWaitOn.length - alreadySynced;
|
|
313
|
+
if (needSync > 0) {
|
|
314
|
+
debug(`waitForSync: ${alreadySynced} already synced, waiting for ${needSync} remaining`);
|
|
315
|
+
out.taskLine(`Uploading: ${alreadySynced}/${handlesToWaitOn.length} already synced, waiting for ${needSync} more`);
|
|
316
|
+
} else {
|
|
317
|
+
debug(`waitForSync: all ${handlesToWaitOn.length} already synced`);
|
|
318
|
+
}
|
|
319
|
+
|
|
263
320
|
try {
|
|
264
321
|
await Promise.all(promises);
|
|
265
|
-
|
|
322
|
+
const elapsed = Date.now() - startTime;
|
|
323
|
+
debug(`waitForSync: all ${handlesToWaitOn.length} documents synced in ${elapsed}ms (${alreadySynced} were already synced)`);
|
|
324
|
+
out.taskLine(`All ${handlesToWaitOn.length} documents uploaded to server (${(elapsed / 1000).toFixed(1)}s)`);
|
|
266
325
|
} catch (error) {
|
|
267
326
|
const elapsed = Date.now() - startTime;
|
|
268
327
|
debug(`waitForSync: failed after ${elapsed}ms: ${error}`);
|
|
328
|
+
out.taskLine(`Upload to server failed after ${(elapsed / 1000).toFixed(1)}s: ${error}`, true);
|
|
269
329
|
throw error;
|
|
270
330
|
}
|
|
271
331
|
}
|