-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDoubleSyncDiff.js
More file actions
127 lines (114 loc) · 4.35 KB
/
Copy pathDoubleSyncDiff.js
File metadata and controls
127 lines (114 loc) · 4.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import { equalBytes } from './internal.js';
/**
* Pure diff algorithm: given two parsed bundles (V_prev and V_new), produce an
* ordered list of operations that turn V_prev into V_new.
*
* Op set:
* `replace_file(path, manifest, fingerprint)` — file content changed
* `add_file(path, manifest, fingerprint)` — new file (intermediate folders implicit)
* `add_empty_folder(path)` — empty folder (non-empty are implied by add_file)
* `remove(path)` — file or folder removed (recursive for folders)
*
* Identity check: folders whose 32-byte fingerprint matches are skipped entirely
* (zero ops emitted for the whole subtree). That's where the size win comes from.
*
* @typedef {Object} DiffOp
* @property {'replace_file' | 'add_file' | 'add_empty_folder' | 'remove'} kind
* @property {string[]} path - segments from root, exclusive of root
* @property {Uint8Array} [manifest] - raw CDCManifest bytes (for *_file ops)
* @property {Uint8Array} [fingerprint] - 32 bytes (for *_file ops)
*/
/**
* Compare two parsed bundles and return the ops that take prev → next.
*
* Children at every level are matched by `name` (not by index), so a file added
* before another file in the same folder produces just an `add_file` op rather
* than churning the second file.
*
* @param {import('./DoubleSyncSnapshot.js').default} prevSnapshot
* @param {import('./DoubleSyncSnapshot.js').default} nextSnapshot
* @returns {DiffOp[]}
*/
export function diffSnapshots(prevSnapshot, nextSnapshot) {
return diffChildren(prevSnapshot.rootChildren, nextSnapshot.rootChildren, []);
}
/**
* Recurse into two ordered arrays of snapshot entries.
*
* @param {Array<Object>} prevEntries
* @param {Array<Object>} nextEntries
* @param {string[]} prefix - path from root to the folder these entries belong to
* @returns {DiffOp[]}
*/
function diffChildren(prevEntries, nextEntries, prefix) {
/** @type {DiffOp[]} */
const ops = [];
const prevByName = new Map(prevEntries.map(e => [e.name, e]));
const nextByName = new Map(nextEntries.map(e => [e.name, e]));
// Removes first — apply them in stable name order for determinism.
const removedNames = [...prevByName.keys()].filter(n => !nextByName.has(n)).sort();
for (const name of removedNames) {
ops.push({ kind: 'remove', path: [...prefix, name] });
}
// Then process the next-side children, in stable order.
const nextNames = [...nextByName.keys()].sort();
for (const name of nextNames) {
const next = nextByName.get(name);
const prev = prevByName.get(name);
const here = [...prefix, name];
if (!prev) {
// brand new entry
emitAdds(next, here, ops);
continue;
}
if (prev.kind !== next.kind) {
// type change at same path — remove old, add new
ops.push({ kind: 'remove', path: here });
emitAdds(next, here, ops);
continue;
}
if (equalBytes(prev.fingerprint, next.fingerprint)) {
// identical subtree (folder) or identical content (file) — no ops
continue;
}
if (next.kind === 'file') {
ops.push({
kind: 'replace_file',
path: here,
manifest: next.manifestBytes,
fingerprint: next.fingerprint,
});
} else {
// folder with same name but different fingerprint → recurse
ops.push(...diffChildren(prev.children, next.children, here));
}
}
return ops;
}
/**
* Emit add ops for an entry that wasn't present on the prev side.
*
* @param {Object} entry - snapshot entry (file or folder)
* @param {string[]} path
* @param {DiffOp[]} sink - ops accumulator
*/
function emitAdds(entry, path, sink) {
if (entry.kind === 'file') {
sink.push({
kind: 'add_file',
path,
manifest: entry.manifestBytes,
fingerprint: entry.fingerprint,
});
return;
}
// folder
if (entry.children.length === 0) {
sink.push({ kind: 'add_empty_folder', path });
return;
}
// non-empty folder: recurse — children's adds implicitly create this folder
for (const child of entry.children) {
emitAdds(child, [...path, child.name], sink);
}
}