Skip to content

Commit 00d6416

Browse files
committed
Support queueing renaming, moving files
1 parent a78582a commit 00d6416

2 files changed

Lines changed: 185 additions & 4 deletions

File tree

src/sync/SyncManager.ts

Lines changed: 171 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,180 @@ function parseOfflineQueuePayload(raw: string): SyncQueueEntry[] {
159159
throw new Error('Invalid offline queue payload');
160160
}
161161

162-
return payload.queue
162+
const normalized = payload.queue
163163
.filter(isQueueEntry)
164164
.map(entry => ({
165165
...entry,
166166
path: normalizePath(entry.path),
167167
oldPath: entry.oldPath ? normalizePath(entry.oldPath) : undefined,
168168
}));
169+
return compactQueueEntries(normalized);
170+
}
171+
172+
function findLastQueuedEntryByPath(queue: SyncQueueEntry[], path: string): number {
173+
for (let index = queue.length - 1; index >= 0; index -= 1) {
174+
if (queue[index]?.path === path) {
175+
return index;
176+
}
177+
}
178+
return -1;
179+
}
180+
181+
function compactQueueEntries(queue: SyncQueueEntry[]): SyncQueueEntry[] {
182+
const compacted: SyncQueueEntry[] = [];
183+
184+
const appendDelete = (entry: SyncQueueEntry): void => {
185+
let targetPath = entry.path;
186+
while (true) {
187+
const existingIndex = findLastQueuedEntryByPath(compacted, targetPath);
188+
if (existingIndex < 0) {
189+
compacted.push({
190+
...entry,
191+
action: 'delete',
192+
path: targetPath,
193+
oldPath: undefined,
194+
});
195+
return;
196+
}
197+
198+
const existing = compacted[existingIndex]!;
199+
if (existing.action === 'create') {
200+
compacted.splice(existingIndex, 1);
201+
return;
202+
}
203+
if (existing.action === 'update') {
204+
compacted[existingIndex] = {
205+
...entry,
206+
action: 'delete',
207+
path: targetPath,
208+
oldPath: undefined,
209+
};
210+
return;
211+
}
212+
if (existing.action === 'delete') {
213+
return;
214+
}
215+
if (existing.action === 'rename' && existing.oldPath) {
216+
targetPath = existing.oldPath;
217+
compacted.splice(existingIndex, 1);
218+
continue;
219+
}
220+
221+
compacted[existingIndex] = {
222+
...entry,
223+
action: 'delete',
224+
path: targetPath,
225+
oldPath: undefined,
226+
};
227+
return;
228+
}
229+
};
230+
231+
for (const rawEntry of queue) {
232+
const entry: SyncQueueEntry = {
233+
...rawEntry,
234+
path: normalizePath(rawEntry.path),
235+
oldPath: rawEntry.oldPath ? normalizePath(rawEntry.oldPath) : undefined,
236+
};
237+
238+
if (entry.action === 'create' || entry.action === 'update') {
239+
const existingIndex = findLastQueuedEntryByPath(compacted, entry.path);
240+
if (existingIndex < 0) {
241+
compacted.push({
242+
...entry,
243+
oldPath: undefined,
244+
});
245+
continue;
246+
}
247+
248+
const existing = compacted[existingIndex]!;
249+
if (existing.action === 'create') {
250+
compacted[existingIndex] = {
251+
...existing,
252+
timestamp: entry.timestamp,
253+
retryCount: entry.retryCount,
254+
localHash: entry.localHash,
255+
};
256+
continue;
257+
}
258+
if (existing.action === 'update' || existing.action === 'delete') {
259+
compacted[existingIndex] = {
260+
...entry,
261+
action: 'update',
262+
oldPath: undefined,
263+
};
264+
continue;
265+
}
266+
if (existing.action === 'rename') {
267+
// Rename already includes content hash of destination path.
268+
continue;
269+
}
270+
271+
compacted.push({
272+
...entry,
273+
oldPath: undefined,
274+
});
275+
continue;
276+
}
277+
278+
if (entry.action === 'delete') {
279+
appendDelete(entry);
280+
continue;
281+
}
282+
283+
const oldPath = entry.oldPath;
284+
if (!oldPath || oldPath === entry.path) {
285+
continue;
286+
}
287+
288+
const duplicateDestinationIndex = findLastQueuedEntryByPath(compacted, entry.path);
289+
if (duplicateDestinationIndex >= 0) {
290+
const duplicateDestination = compacted[duplicateDestinationIndex]!;
291+
if (duplicateDestination.action === 'create' || duplicateDestination.action === 'update') {
292+
compacted.splice(duplicateDestinationIndex, 1);
293+
}
294+
}
295+
296+
const sourceIndex = findLastQueuedEntryByPath(compacted, oldPath);
297+
if (sourceIndex < 0) {
298+
compacted.push(entry);
299+
continue;
300+
}
301+
302+
const source = compacted[sourceIndex]!;
303+
if (source.action === 'create') {
304+
compacted[sourceIndex] = {
305+
...source,
306+
path: entry.path,
307+
timestamp: entry.timestamp,
308+
retryCount: entry.retryCount,
309+
localHash: entry.localHash ?? source.localHash,
310+
};
311+
continue;
312+
}
313+
if (source.action === 'update') {
314+
compacted[sourceIndex] = {
315+
...entry,
316+
action: 'rename',
317+
oldPath,
318+
};
319+
continue;
320+
}
321+
if (source.action === 'rename' && source.oldPath) {
322+
compacted[sourceIndex] = {
323+
...source,
324+
path: entry.path,
325+
timestamp: entry.timestamp,
326+
retryCount: entry.retryCount,
327+
localHash: entry.localHash ?? source.localHash,
328+
};
329+
continue;
330+
}
331+
332+
compacted.push(entry);
333+
}
334+
335+
return compacted;
169336
}
170337

171338
function isActivityAction(value: unknown): value is ActivityAction {
@@ -1675,7 +1842,7 @@ export class SyncManager {
16751842
this.cancelModifyDebouncer(normalizedEntry.oldPath);
16761843
}
16771844

1678-
this.pendingPushQueue.push(normalizedEntry);
1845+
this.pendingPushQueue = compactQueueEntries([...this.pendingPushQueue, normalizedEntry]);
16791846
this.updateStatusFromCurrentState();
16801847
this.schedulePushQueueProcessing();
16811848
}
@@ -1998,7 +2165,7 @@ export class SyncManager {
19982165

19992166
this.replayInFlight = true;
20002167
try {
2001-
this.pendingPushQueue = [...this.offlineQueue, ...this.pendingPushQueue];
2168+
this.pendingPushQueue = compactQueueEntries([...this.offlineQueue, ...this.pendingPushQueue]);
20022169
this.offlineQueue = [];
20032170
await this.persistOfflineQueue();
20042171
return this.flushPendingPushQueue();
@@ -2012,7 +2179,7 @@ export class SyncManager {
20122179
return;
20132180
}
20142181

2015-
this.offlineQueue.push(...this.pendingPushQueue);
2182+
this.offlineQueue = compactQueueEntries([...this.offlineQueue, ...this.pendingPushQueue]);
20162183
this.pendingPushQueue = [];
20172184
await this.persistOfflineQueue();
20182185
}

src/sync/UploadManager.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,13 @@ export class UploadManager {
208208
}
209209

210210
const localHash = await computeContentHash(await this.plugin.app.vault.adapter.readBinary(normalizedNewPath));
211+
const sourceRecord = this.syncDb.getRecord(normalizedOldPath);
212+
if (!sourceRecord) {
213+
const destinationRecord = this.syncDb.getRecord(normalizedNewPath);
214+
if (destinationRecord?.localHash === localHash) {
215+
return false;
216+
}
217+
}
211218
await this.applyRename(normalizedOldPath, normalizedNewPath, localHash);
212219
return true;
213220
}
@@ -339,6 +346,13 @@ export class UploadManager {
339346
private async applyRename(oldPath: string, newPath: string, localHash: string): Promise<void> {
340347
const record = this.syncDb.getRecord(oldPath);
341348
if (!record) {
349+
const existingTargetRecord = this.syncDb.getRecord(newPath);
350+
if (existingTargetRecord) {
351+
if (existingTargetRecord.localHash !== localHash) {
352+
await this.pushExistingFile(newPath, localHash);
353+
}
354+
return;
355+
}
342356
await this.pushNewFile(newPath, localHash);
343357
return;
344358
}

0 commit comments

Comments
 (0)